From 0234f75495b0320dddda5006b25b617bfad7e9ce Mon Sep 17 00:00:00 2001 From: Roger Date: Sat, 16 May 2026 14:46:18 +0800 Subject: [PATCH] refactor(chromium): isolate Yandex protocol specifics design (#598) --- browser/chromium/extract_creditcard.go | 11 --- browser/chromium/extract_password.go | 24 ----- browser/chromium/source.go | 25 ------ browser/chromium/yandex.go | 116 +++++++++++++++++++++++++ browser/chromium/yandex_key.go | 55 ------------ 5 files changed, 116 insertions(+), 115 deletions(-) create mode 100644 browser/chromium/yandex.go delete mode 100644 browser/chromium/yandex_key.go diff --git a/browser/chromium/extract_creditcard.go b/browser/chromium/extract_creditcard.go index 2135c75..d4374c9 100644 --- a/browser/chromium/extract_creditcard.go +++ b/browser/chromium/extract_creditcard.go @@ -119,14 +119,3 @@ func countCreditCards(path string) (int, error) { func countYandexCreditCards(path string) (int, error) { return sqliteutil.CountRows(path, false, yandexCreditCardCountQuery) } - -// yandexCardAAD is the raw guid bytes (+ keyID if the profile has a master password). -func yandexCardAAD(guid string, keyID []byte) []byte { - if len(keyID) == 0 { - return []byte(guid) - } - out := make([]byte, 0, len(guid)+len(keyID)) - out = append(out, guid...) - out = append(out, keyID...) - return out -} diff --git a/browser/chromium/extract_password.go b/browser/chromium/extract_password.go index a7c3cb2..8a58f13 100644 --- a/browser/chromium/extract_password.go +++ b/browser/chromium/extract_password.go @@ -1,7 +1,6 @@ package chromium import ( - "crypto/sha1" "database/sql" "errors" "sort" @@ -99,26 +98,3 @@ func extractYandexPasswords(keys keyretriever.MasterKeys, path string) ([]types. func countPasswords(path string) (int, error) { return sqliteutil.CountRows(path, false, countLoginQuery) } - -// yandexLoginAAD is SHA1(origin_url \x00 username_element \x00 username_value \x00 password_element \x00 signon_realm), -// with keyID appended when the profile has a master password (v1 always passes nil). -func yandexLoginAAD(originURL, usernameElem, usernameVal, passwordElem, signonRealm string, keyID []byte) []byte { - h := sha1.New() - h.Write([]byte(originURL)) - h.Write([]byte{0}) - h.Write([]byte(usernameElem)) - h.Write([]byte{0}) - h.Write([]byte(usernameVal)) - h.Write([]byte{0}) - h.Write([]byte(passwordElem)) - h.Write([]byte{0}) - h.Write([]byte(signonRealm)) - sum := h.Sum(nil) - if len(keyID) == 0 { - return sum - } - out := make([]byte, 0, len(sum)+len(keyID)) - out = append(out, sum...) - out = append(out, keyID...) - return out -} diff --git a/browser/chromium/source.go b/browser/chromium/source.go index 93b71fa..acc7f3e 100644 --- a/browser/chromium/source.go +++ b/browser/chromium/source.go @@ -32,24 +32,6 @@ var chromiumSources = map[types.Category][]sourcePath{ types.SessionStorage: {dir("Session Storage")}, } -// yandexSourceOverrides contains only the entries that differ from chromiumSources. -var yandexSourceOverrides = map[types.Category][]sourcePath{ - types.Password: {file("Ya Passman Data")}, - types.CreditCard: {file("Ya Credit Cards")}, -} - -// yandexSources returns chromiumSources with Yandex-specific overrides applied. -func yandexSources() map[types.Category][]sourcePath { - sources := make(map[types.Category][]sourcePath, len(chromiumSources)) - for k, v := range chromiumSources { - sources[k] = v - } - for k, v := range yandexSourceOverrides { - sources[k] = v - } - return sources -} - // sourcesForKind returns the source mapping for a browser kind. func sourcesForKind(kind types.BrowserKind) map[types.Category][]sourcePath { switch kind { @@ -106,13 +88,6 @@ func (e creditCardExtractor) extract(keys keyretriever.MasterKeys, path string, return err } -// yandexExtractors overrides Password and CreditCard extraction for Yandex, which wraps its data-encryption key inside -// meta.local_encryptor_data, binds per-row AAD to GCM, and stores cards as JSON blobs in a records table. -var yandexExtractors = map[types.Category]categoryExtractor{ - types.Password: passwordExtractor{fn: extractYandexPasswords}, - types.CreditCard: creditCardExtractor{fn: extractYandexCreditCards}, -} - // operaExtractors overrides Extension extraction for Opera, // which stores settings under "extensions.opsettings". var operaExtractors = map[types.Category]categoryExtractor{ diff --git a/browser/chromium/yandex.go b/browser/chromium/yandex.go new file mode 100644 index 0000000..d2eb16b --- /dev/null +++ b/browser/chromium/yandex.go @@ -0,0 +1,116 @@ +package chromium + +import ( + "crypto/sha1" + "database/sql" + "errors" + "fmt" + "os" + "strings" + + _ "modernc.org/sqlite" + + "github.com/moond4rk/hackbrowserdata/crypto" + "github.com/moond4rk/hackbrowserdata/types" +) + +// yandexSourceOverrides contains only the entries that differ from chromiumSources. +var yandexSourceOverrides = map[types.Category][]sourcePath{ + types.Password: {file("Ya Passman Data")}, + types.CreditCard: {file("Ya Credit Cards")}, +} + +// yandexSources returns chromiumSources with Yandex-specific overrides applied. +func yandexSources() map[types.Category][]sourcePath { + sources := make(map[types.Category][]sourcePath, len(chromiumSources)) + for k, v := range chromiumSources { + sources[k] = v + } + for k, v := range yandexSourceOverrides { + sources[k] = v + } + return sources +} + +// yandexExtractors overrides Password and CreditCard extraction for Yandex, which wraps its data-encryption key inside +// meta.local_encryptor_data, binds per-row AAD to GCM, and stores cards as JSON blobs in a records table. +var yandexExtractors = map[types.Category]categoryExtractor{ + types.Password: passwordExtractor{fn: extractYandexPasswords}, + types.CreditCard: creditCardExtractor{fn: extractYandexCreditCards}, +} + +// yandexLoginAAD is SHA1(origin_url \x00 username_element \x00 username_value \x00 password_element \x00 signon_realm), +// with keyID appended when the profile has a master password (v1 always passes nil). +func yandexLoginAAD(originURL, usernameElem, usernameVal, passwordElem, signonRealm string, keyID []byte) []byte { + h := sha1.New() + h.Write([]byte(originURL)) + h.Write([]byte{0}) + h.Write([]byte(usernameElem)) + h.Write([]byte{0}) + h.Write([]byte(usernameVal)) + h.Write([]byte{0}) + h.Write([]byte(passwordElem)) + h.Write([]byte{0}) + h.Write([]byte(signonRealm)) + sum := h.Sum(nil) + if len(keyID) == 0 { + return sum + } + out := make([]byte, 0, len(sum)+len(keyID)) + out = append(out, sum...) + out = append(out, keyID...) + return out +} + +// yandexCardAAD is the raw guid bytes (+ keyID if the profile has a master password). +func yandexCardAAD(guid string, keyID []byte) []byte { + if len(keyID) == 0 { + return []byte(guid) + } + out := make([]byte, 0, len(guid)+len(keyID)) + out = append(out, guid...) + out = append(out, keyID...) + return out +} + +// errYandexMasterPasswordSet: caller warns + skips; RSA-OAEP unseal is deferred (RFC-012 §6). +var errYandexMasterPasswordSet = errors.New("yandex: profile protected by master password, skipping") + +// loadYandexDataKey honors the master-password gate and returns the per-DB data key. See RFC-012 §4.2. +func loadYandexDataKey(dbPath string, masterKey []byte) ([]byte, error) { + if len(masterKey) == 0 { + return nil, fmt.Errorf("yandex: master key not available") + } + if _, err := os.Stat(dbPath); err != nil { + return nil, fmt.Errorf("yandex db file: %w", err) + } + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return nil, err + } + defer db.Close() + + if hasMasterPassword(db) { + return nil, errYandexMasterPasswordSet + } + + var blob []byte + if err := db.QueryRow("SELECT value FROM meta WHERE key = 'local_encryptor_data'").Scan(&blob); err != nil { + return nil, fmt.Errorf("read local_encryptor_data: %w", err) + } + + dataKey, err := crypto.DecryptYandexIntermediateKey(masterKey, blob) + if err != nil { + return nil, fmt.Errorf("derive yandex data key: %w", err) + } + return dataKey, nil +} + +// hasMasterPassword: missing table (Ya Credit Cards) or empty sealed_key both mean false. +func hasMasterPassword(db *sql.DB) bool { + var sealed sql.NullString + if err := db.QueryRow("SELECT sealed_key FROM active_keys").Scan(&sealed); err != nil { + return false + } + return sealed.Valid && strings.TrimSpace(sealed.String) != "" +} diff --git a/browser/chromium/yandex_key.go b/browser/chromium/yandex_key.go deleted file mode 100644 index 629da17..0000000 --- a/browser/chromium/yandex_key.go +++ /dev/null @@ -1,55 +0,0 @@ -package chromium - -import ( - "database/sql" - "errors" - "fmt" - "os" - "strings" - - _ "modernc.org/sqlite" - - "github.com/moond4rk/hackbrowserdata/crypto" -) - -// errYandexMasterPasswordSet: caller warns + skips; RSA-OAEP unseal is deferred (RFC-012 §6). -var errYandexMasterPasswordSet = errors.New("yandex: profile protected by master password, skipping") - -// loadYandexDataKey honors the master-password gate and returns the per-DB data key. See RFC-012 §4.2. -func loadYandexDataKey(dbPath string, masterKey []byte) ([]byte, error) { - if len(masterKey) == 0 { - return nil, fmt.Errorf("yandex: master key not available") - } - if _, err := os.Stat(dbPath); err != nil { - return nil, fmt.Errorf("yandex db file: %w", err) - } - db, err := sql.Open("sqlite", dbPath) - if err != nil { - return nil, err - } - defer db.Close() - - if hasMasterPassword(db) { - return nil, errYandexMasterPasswordSet - } - - var blob []byte - if err := db.QueryRow("SELECT value FROM meta WHERE key = 'local_encryptor_data'").Scan(&blob); err != nil { - return nil, fmt.Errorf("read local_encryptor_data: %w", err) - } - - dataKey, err := crypto.DecryptYandexIntermediateKey(masterKey, blob) - if err != nil { - return nil, fmt.Errorf("derive yandex data key: %w", err) - } - return dataKey, nil -} - -// hasMasterPassword: missing table (Ya Credit Cards) or empty sealed_key both mean false. -func hasMasterPassword(db *sql.DB) bool { - var sealed sql.NullString - if err := db.QueryRow("SELECT sealed_key FROM active_keys").Scan(&sealed); err != nil { - return false - } - return sealed.Valid && strings.TrimSpace(sealed.String) != "" -}