Skip to content

Commit 0c6c781

Browse files
authored
feat(yandex): password and credit card decryption (#585)
1 parent 7e64d50 commit 0c6c781

17 files changed

Lines changed: 1005 additions & 48 deletions

browser/chromium/chromium.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ func (b *Browser) countCategory(cat types.Category, path string) int {
138138
case types.Bookmark:
139139
count, err = countBookmarks(path)
140140
case types.CreditCard:
141-
count, err = countCreditCards(path)
141+
if b.cfg.Kind == types.ChromiumYandex {
142+
count, err = countYandexCreditCards(path)
143+
} else {
144+
count, err = countCreditCards(path)
145+
}
142146
case types.Extension:
143147
if b.cfg.Kind == types.ChromiumOpera {
144148
count, err = countOperaExtensions(path)

browser/chromium/chromium_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ func TestExtractorsForKind(t *testing.T) {
337337
yandexExt := extractorsForKind(types.ChromiumYandex)
338338
require.NotNil(t, yandexExt)
339339
assert.Contains(t, yandexExt, types.Password)
340+
assert.Contains(t, yandexExt, types.CreditCard)
340341

341342
operaExt := extractorsForKind(types.ChromiumOpera)
342343
require.NotNil(t, operaExt)

browser/chromium/extract_creditcard.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ package chromium
22

33
import (
44
"database/sql"
5+
"encoding/json"
6+
"errors"
57

8+
"github.com/moond4rk/hackbrowserdata/crypto"
69
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
10+
"github.com/moond4rk/hackbrowserdata/log"
711
"github.com/moond4rk/hackbrowserdata/types"
812
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
913
)
@@ -12,8 +16,26 @@ const (
1216
defaultCreditCardQuery = `SELECT COALESCE(guid, ''), name_on_card, expiration_month, expiration_year,
1317
card_number_encrypted, COALESCE(nickname, ''), COALESCE(billing_address_id, '') FROM credit_cards`
1418
countCreditCardQuery = `SELECT COUNT(*) FROM credit_cards`
19+
20+
yandexCreditCardQuery = `SELECT guid, public_data, private_data FROM records`
21+
yandexCreditCardCountQuery = `SELECT COUNT(*) FROM records`
1522
)
1623

24+
// yandexPublicData is the plaintext JSON in records.public_data.
25+
type yandexPublicData struct {
26+
CardHolder string `json:"card_holder"`
27+
CardTitle string `json:"card_title"`
28+
ExpireDateYear string `json:"expire_date_year"`
29+
ExpireDateMonth string `json:"expire_date_month"`
30+
}
31+
32+
// yandexPrivateData is the AES-GCM-sealed JSON in records.private_data.
33+
type yandexPrivateData struct {
34+
FullCardNumber string `json:"full_card_number"`
35+
PinCode string `json:"pin_code"`
36+
SecretComment string `json:"secret_comment"`
37+
}
38+
1739
func extractCreditCards(keys keyretriever.MasterKeys, path string) ([]types.CreditCardEntry, error) {
1840
cards, err := sqliteutil.QueryRows(path, false, defaultCreditCardQuery,
1941
func(rows *sql.Rows) (types.CreditCardEntry, error) {
@@ -39,6 +61,72 @@ func extractCreditCards(keys keyretriever.MasterKeys, path string) ([]types.Cred
3961
return cards, nil
4062
}
4163

64+
// extractYandexCreditCards reads the records table (not Chromium's credit_cards). AAD = guid. See RFC-012 §4.
65+
func extractYandexCreditCards(keys keyretriever.MasterKeys, path string) ([]types.CreditCardEntry, error) {
66+
dataKey, err := loadYandexDataKey(path, keys.V10)
67+
if err != nil {
68+
if errors.Is(err, errYandexMasterPasswordSet) {
69+
log.Warnf("%s: %v", path, err)
70+
return nil, nil
71+
}
72+
return nil, err
73+
}
74+
75+
return sqliteutil.QueryRows(path, false, yandexCreditCardQuery,
76+
func(rows *sql.Rows) (types.CreditCardEntry, error) {
77+
var guid, publicData string
78+
var privateData []byte
79+
if err := rows.Scan(&guid, &publicData, &privateData); err != nil {
80+
return types.CreditCardEntry{}, err
81+
}
82+
83+
var public yandexPublicData
84+
if publicData != "" {
85+
if err := json.Unmarshal([]byte(publicData), &public); err != nil {
86+
log.Debugf("yandex: parse public_data for %s: %v", guid, err)
87+
}
88+
}
89+
entry := types.CreditCardEntry{
90+
GUID: guid,
91+
Name: public.CardHolder,
92+
ExpMonth: public.ExpireDateMonth,
93+
ExpYear: public.ExpireDateYear,
94+
NickName: public.CardTitle,
95+
}
96+
97+
plaintext, err := crypto.AESGCMDecryptBlob(dataKey, privateData, yandexCardAAD(guid, nil))
98+
if err != nil {
99+
log.Debugf("yandex: decrypt card %s: %v", guid, err)
100+
return entry, nil
101+
}
102+
103+
var private yandexPrivateData
104+
if err := json.Unmarshal(plaintext, &private); err != nil {
105+
log.Debugf("yandex: parse private_data for %s: %v", guid, err)
106+
return entry, nil
107+
}
108+
entry.Number = private.FullCardNumber
109+
entry.CVC = private.PinCode
110+
entry.Comment = private.SecretComment
111+
return entry, nil
112+
})
113+
}
114+
42115
func countCreditCards(path string) (int, error) {
43116
return sqliteutil.CountRows(path, false, countCreditCardQuery)
44117
}
118+
119+
func countYandexCreditCards(path string) (int, error) {
120+
return sqliteutil.CountRows(path, false, yandexCreditCardCountQuery)
121+
}
122+
123+
// yandexCardAAD is the raw guid bytes (+ keyID if the profile has a master password).
124+
func yandexCardAAD(guid string, keyID []byte) []byte {
125+
if len(keyID) == 0 {
126+
return []byte(guid)
127+
}
128+
out := make([]byte, 0, len(guid)+len(keyID))
129+
out = append(out, guid...)
130+
out = append(out, keyID...)
131+
return out
132+
}

browser/chromium/extract_creditcard_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package chromium
22

33
import (
4+
"bytes"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -51,3 +52,90 @@ func TestCountCreditCards_Empty(t *testing.T) {
5152
require.NoError(t, err)
5253
assert.Equal(t, 0, count)
5354
}
55+
56+
func TestExtractYandexCreditCards(t *testing.T) {
57+
masterKey := bytes.Repeat([]byte{0x11}, 32)
58+
dataKey := bytes.Repeat([]byte{0x22}, 32)
59+
60+
path := setupYandexCreditCardDB(t, masterKey, dataKey,
61+
yandexCreditCard{
62+
GUID: "card-1",
63+
CardHolder: "Alice Smith",
64+
CardTitle: "Personal Visa",
65+
ExpYear: "2030",
66+
ExpMonth: "06",
67+
FullCardNumber: "4111111111111111",
68+
PinCode: "123",
69+
SecretComment: "main card",
70+
},
71+
yandexCreditCard{
72+
GUID: "card-2",
73+
CardHolder: "Alice Smith",
74+
CardTitle: "Backup",
75+
ExpYear: "2028",
76+
ExpMonth: "12",
77+
FullCardNumber: "5555555555554444",
78+
PinCode: "456",
79+
SecretComment: "",
80+
},
81+
)
82+
83+
got, err := extractYandexCreditCards(keyretriever.MasterKeys{V10: masterKey}, path)
84+
require.NoError(t, err)
85+
require.Len(t, got, 2)
86+
87+
byGUID := map[string]int{}
88+
for i, c := range got {
89+
byGUID[c.GUID] = i
90+
}
91+
92+
c1 := got[byGUID["card-1"]]
93+
assert.Equal(t, "Alice Smith", c1.Name)
94+
assert.Equal(t, "Personal Visa", c1.NickName)
95+
assert.Equal(t, "2030", c1.ExpYear)
96+
assert.Equal(t, "06", c1.ExpMonth)
97+
assert.Equal(t, "4111111111111111", c1.Number)
98+
assert.Equal(t, "123", c1.CVC)
99+
assert.Equal(t, "main card", c1.Comment)
100+
101+
c2 := got[byGUID["card-2"]]
102+
assert.Equal(t, "5555555555554444", c2.Number)
103+
assert.Equal(t, "456", c2.CVC)
104+
assert.Empty(t, c2.Comment)
105+
}
106+
107+
func TestCountYandexCreditCards(t *testing.T) {
108+
masterKey := bytes.Repeat([]byte{0x11}, 32)
109+
dataKey := bytes.Repeat([]byte{0x22}, 32)
110+
111+
path := setupYandexCreditCardDB(t, masterKey, dataKey,
112+
yandexCreditCard{GUID: "g1", FullCardNumber: "x"},
113+
yandexCreditCard{GUID: "g2", FullCardNumber: "y"},
114+
yandexCreditCard{GUID: "g3", FullCardNumber: "z"},
115+
)
116+
117+
count, err := countYandexCreditCards(path)
118+
require.NoError(t, err)
119+
assert.Equal(t, 3, count)
120+
}
121+
122+
func TestExtractYandexCreditCards_WrongMasterKey(t *testing.T) {
123+
goodKey := bytes.Repeat([]byte{0x11}, 32)
124+
wrongKey := bytes.Repeat([]byte{0x99}, 32)
125+
dataKey := bytes.Repeat([]byte{0x22}, 32)
126+
127+
path := setupYandexCreditCardDB(t, goodKey, dataKey,
128+
yandexCreditCard{GUID: "g1", FullCardNumber: "4111"},
129+
)
130+
131+
_, err := extractYandexCreditCards(keyretriever.MasterKeys{V10: wrongKey}, path)
132+
require.Error(t, err)
133+
}
134+
135+
func TestYandexCardAAD(t *testing.T) {
136+
got := yandexCardAAD("card-guid-1", nil)
137+
assert.Equal(t, "card-guid-1", string(got))
138+
139+
got = yandexCardAAD("g", []byte("ID"))
140+
assert.Equal(t, "gID", string(got))
141+
}

browser/chromium/extract_password.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
package chromium
22

33
import (
4+
"crypto/sha1"
45
"database/sql"
6+
"errors"
57
"sort"
68

9+
"github.com/moond4rk/hackbrowserdata/crypto"
710
"github.com/moond4rk/hackbrowserdata/crypto/keyretriever"
11+
"github.com/moond4rk/hackbrowserdata/log"
812
"github.com/moond4rk/hackbrowserdata/types"
913
"github.com/moond4rk/hackbrowserdata/utils/sqliteutil"
1014
)
1115

1216
const (
1317
defaultLoginQuery = `SELECT origin_url, username_value, password_value, date_created FROM logins`
1418
countLoginQuery = `SELECT COUNT(*) FROM logins`
19+
20+
yandexLoginQuery = `SELECT origin_url, username_element, username_value,
21+
password_element, password_value, signon_realm, date_created FROM logins`
1522
)
1623

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

48-
// extractYandexPasswords extracts passwords from Yandex's Ya Passman Data, which stores the URL in
49-
// action_url instead of origin_url.
55+
// extractYandexPasswords walks Ya Passman Data; protocol in RFC-012 §4.
56+
// Note: URL column is origin_url — it's what the per-row AAD is computed over (not action_url).
5057
func extractYandexPasswords(keys keyretriever.MasterKeys, path string) ([]types.LoginEntry, error) {
51-
const yandexLoginQuery = `SELECT action_url, username_value, password_value, date_created FROM logins`
52-
return extractPasswordsWithQuery(keys, path, yandexLoginQuery)
58+
dataKey, err := loadYandexDataKey(path, keys.V10)
59+
if err != nil {
60+
if errors.Is(err, errYandexMasterPasswordSet) {
61+
log.Warnf("%s: %v", path, err)
62+
return nil, nil
63+
}
64+
return nil, err
65+
}
66+
67+
logins, err := sqliteutil.QueryRows(path, false, yandexLoginQuery,
68+
func(rows *sql.Rows) (types.LoginEntry, error) {
69+
var originURL, usernameElem, usernameVal, passwordElem, signonRealm string
70+
var passwordValue []byte
71+
var created int64
72+
if err := rows.Scan(&originURL, &usernameElem, &usernameVal, &passwordElem, &passwordValue, &signonRealm, &created); err != nil {
73+
return types.LoginEntry{}, err
74+
}
75+
entry := types.LoginEntry{
76+
URL: originURL,
77+
Username: usernameVal,
78+
CreatedAt: timeEpoch(created),
79+
}
80+
aad := yandexLoginAAD(originURL, usernameElem, usernameVal, passwordElem, signonRealm, nil)
81+
plaintext, err := crypto.AESGCMDecryptBlob(dataKey, passwordValue, aad)
82+
if err != nil {
83+
log.Debugf("yandex: decrypt password for %s: %v", originURL, err)
84+
return entry, nil
85+
}
86+
entry.Password = string(plaintext)
87+
return entry, nil
88+
})
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
sort.Slice(logins, func(i, j int) bool {
94+
return logins[i].CreatedAt.After(logins[j].CreatedAt)
95+
})
96+
return logins, nil
5397
}
5498

5599
func countPasswords(path string) (int, error) {
56100
return sqliteutil.CountRows(path, false, countLoginQuery)
57101
}
102+
103+
// yandexLoginAAD is SHA1(origin_url \x00 username_element \x00 username_value \x00 password_element \x00 signon_realm),
104+
// with keyID appended when the profile has a master password (v1 always passes nil).
105+
func yandexLoginAAD(originURL, usernameElem, usernameVal, passwordElem, signonRealm string, keyID []byte) []byte {
106+
h := sha1.New()
107+
h.Write([]byte(originURL))
108+
h.Write([]byte{0})
109+
h.Write([]byte(usernameElem))
110+
h.Write([]byte{0})
111+
h.Write([]byte(usernameVal))
112+
h.Write([]byte{0})
113+
h.Write([]byte(passwordElem))
114+
h.Write([]byte{0})
115+
h.Write([]byte(signonRealm))
116+
sum := h.Sum(nil)
117+
if len(keyID) == 0 {
118+
return sum
119+
}
120+
out := make([]byte, 0, len(sum)+len(keyID))
121+
out = append(out, sum...)
122+
out = append(out, keyID...)
123+
return out
124+
}

0 commit comments

Comments
 (0)