mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-21 19:06:47 +02:00
feat: add Safari password extraction from macOS Keychain (#568)
This commit is contained in:
@@ -1,3 +1,12 @@
|
||||
// Package keyretriever owns the master-key acquisition chain shared by all
|
||||
// Chromium variants (Chrome, Edge, Brave, Arc, Opera, Vivaldi, Yandex, …).
|
||||
// The chain is built once per process and reused for every profile.
|
||||
//
|
||||
// Firefox and Safari do not route through this package — Firefox derives
|
||||
// its own keys from key4.db via NSS PBE, and Safari reads InternetPassword
|
||||
// records directly from login.keychain-db. Each browser package owns its
|
||||
// own credential-acquisition strategy; see rfcs/006-key-retrieval-mechanisms.md
|
||||
// §7 for the rationale.
|
||||
package keyretriever
|
||||
|
||||
import (
|
||||
|
||||
@@ -8,16 +8,12 @@ import (
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/moond4rk/keychainbreaker"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
)
|
||||
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
|
||||
@@ -106,41 +102,6 @@ func (r *KeychainPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, erro
|
||||
return findStorageKey(r.records, storage)
|
||||
}
|
||||
|
||||
// TerminalPasswordRetriever prompts for the keychain password interactively
|
||||
// via the terminal using golang.org/x/term (with echo disabled).
|
||||
// Automatically skipped when stdin is not a TTY.
|
||||
type TerminalPasswordRetriever struct {
|
||||
once sync.Once
|
||||
records []keychainbreaker.GenericPassword
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *TerminalPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return nil, fmt.Errorf("terminal: stdin is not a TTY")
|
||||
}
|
||||
|
||||
r.once.Do(func() {
|
||||
fmt.Fprint(os.Stderr, "Enter macOS login password: ")
|
||||
pwd, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Fprintln(os.Stderr)
|
||||
if err != nil {
|
||||
r.err = fmt.Errorf("terminal: read password: %w", err)
|
||||
return
|
||||
}
|
||||
r.records, r.err = loadKeychainRecords(string(pwd))
|
||||
if r.err != nil {
|
||||
log.Warnf("keychain unlock failed with provided password")
|
||||
log.Debugf("keychain unlock detail: %v", r.err)
|
||||
}
|
||||
})
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
return findStorageKey(r.records, storage)
|
||||
}
|
||||
|
||||
// SecurityCmdRetriever uses macOS `security` CLI to query Keychain.
|
||||
// This may trigger a password dialog on macOS. Results are cached
|
||||
// per storage name so each browser's key is fetched only once.
|
||||
@@ -194,22 +155,16 @@ func (r *SecurityCmdRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
|
||||
return darwinParams.deriveKey(secret), nil
|
||||
}
|
||||
|
||||
// DefaultRetriever returns the macOS retriever chain.
|
||||
// The chain tries each method in order until one succeeds:
|
||||
// 1. GcoredumpRetriever — CVE-2025-24204 exploit (root only, non-interactive)
|
||||
// 2. KeychainPasswordRetriever — direct unlock with --keychain-pw flag
|
||||
// 3. TerminalPasswordRetriever — interactive password prompt via terminal
|
||||
// 4. SecurityCmdRetriever — security CLI fallback (may trigger system dialog)
|
||||
// DefaultRetriever returns the macOS retriever chain, tried in order:
|
||||
//
|
||||
// 1. GcoredumpRetriever — CVE-2025-24204 exploit (root only)
|
||||
// 2. KeychainPasswordRetriever — direct unlock, skipped when password is empty
|
||||
// 3. SecurityCmdRetriever — `security` CLI fallback (may trigger a dialog)
|
||||
func DefaultRetriever(keychainPassword string) KeyRetriever {
|
||||
retrievers := []KeyRetriever{
|
||||
&GcoredumpRetriever{},
|
||||
}
|
||||
retrievers := []KeyRetriever{&GcoredumpRetriever{}}
|
||||
if keychainPassword != "" {
|
||||
retrievers = append(retrievers, &KeychainPasswordRetriever{Password: keychainPassword})
|
||||
}
|
||||
retrievers = append(retrievers,
|
||||
&TerminalPasswordRetriever{},
|
||||
&SecurityCmdRetriever{cache: make(map[string]securityResult)},
|
||||
)
|
||||
retrievers = append(retrievers, &SecurityCmdRetriever{cache: make(map[string]securityResult)})
|
||||
return NewChain(retrievers...)
|
||||
}
|
||||
|
||||
@@ -39,13 +39,3 @@ func TestKeychainPasswordRetriever_EmptyPassword(t *testing.T) {
|
||||
assert.Nil(t, key)
|
||||
assert.Contains(t, err.Error(), "keychain password not provided")
|
||||
}
|
||||
|
||||
func TestTerminalPasswordRetriever_NonTTY(t *testing.T) {
|
||||
// In CI/test environments, stdin is not a TTY.
|
||||
// The retriever should return an error so the chain can log it and continue.
|
||||
r := &TerminalPasswordRetriever{}
|
||||
key, err := r.RetrieveKey("Chrome", "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "stdin is not a TTY")
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
@@ -78,8 +78,7 @@ func (r *FallbackRetriever) RetrieveKey(_, _ string) ([]byte, error) {
|
||||
|
||||
// DefaultRetriever returns the Linux retriever chain:
|
||||
// D-Bus Secret Service first, then "peanuts" fallback.
|
||||
// The keychainPassword parameter is unused on Linux.
|
||||
func DefaultRetriever(_ string) KeyRetriever {
|
||||
func DefaultRetriever() KeyRetriever {
|
||||
return NewChain(
|
||||
&DBusRetriever{},
|
||||
&FallbackRetriever{},
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestFallbackRetriever(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDefaultRetriever_Linux(t *testing.T) {
|
||||
r := DefaultRetriever("")
|
||||
r := DefaultRetriever()
|
||||
chain, ok := r.(*ChainRetriever)
|
||||
require.True(t, ok, "DefaultRetriever should return a *ChainRetriever")
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ func (r *DPAPIRetriever) RetrieveKey(_, localStatePath string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// DefaultRetriever returns the Windows retriever (DPAPI only).
|
||||
// The keychainPassword parameter is unused on Windows.
|
||||
func DefaultRetriever(_ string) KeyRetriever {
|
||||
func DefaultRetriever() KeyRetriever {
|
||||
return &DPAPIRetriever{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user