feat(yandex): password and credit card decryption (#585)

This commit is contained in:
Roger
2026-04-23 17:00:09 +08:00
committed by GitHub
parent 7e64d50891
commit 0c6c781567
17 changed files with 1005 additions and 48 deletions
+5 -1
View File
@@ -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)
+1
View File
@@ -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)
+88
View File
@@ -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"
)
@@ -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) {
@@ -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
}
@@ -1,6 +1,7 @@
package chromium
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
@@ -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))
}
+71 -4
View File
@@ -1,10 +1,14 @@
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"
)
@@ -12,6 +16,9 @@ import (
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) {
@@ -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
}
+78 -5
View File
@@ -1,6 +1,8 @@
package chromium
import (
"bytes"
"crypto/sha1"
"testing"
"github.com/stretchr/testify/assert"
@@ -52,12 +54,83 @@ func TestCountPasswords_Empty(t *testing.T) {
}
func TestExtractYandexPasswords(t *testing.T) {
path := createTestDB(t, "Ya Passman Data", loginsSchema,
insertLogin("https://origin.yandex.ru", "https://action.yandex.ru/submit", "user", "", 13350000000000000),
masterKey := bytes.Repeat([]byte{0x11}, 32)
dataKey := bytes.Repeat([]byte{0x22}, 32)
path := setupYandexPasswordDB(t, masterKey, dataKey, false,
yandexPassword{
OriginURL: "https://old.yandex.ru", UsernameElem: "u", UsernameVal: "alice",
PasswordElem: "p", SignonRealm: "https://old.yandex.ru", Password: "hunter2",
DateCreated: 13340000000000000,
},
yandexPassword{
OriginURL: "https://new.yandex.ru", UsernameElem: "u", UsernameVal: "bob",
PasswordElem: "p", SignonRealm: "https://new.yandex.ru", Password: "sesame",
DateCreated: 13360000000000000,
},
)
got, err := extractYandexPasswords(keyretriever.MasterKeys{}, path)
got, err := extractYandexPasswords(keyretriever.MasterKeys{V10: masterKey}, path)
require.NoError(t, err)
require.Len(t, got, 1)
assert.Equal(t, "https://action.yandex.ru/submit", got[0].URL) // action_url, not origin_url
require.Len(t, got, 2)
// Sorted newest-first on CreatedAt.
assert.Equal(t, "https://new.yandex.ru", got[0].URL)
assert.Equal(t, "bob", got[0].Username)
assert.Equal(t, "sesame", got[0].Password)
assert.Equal(t, "hunter2", got[1].Password)
}
func TestExtractYandexPasswords_MasterPasswordSkipped(t *testing.T) {
masterKey := bytes.Repeat([]byte{0x11}, 32)
dataKey := bytes.Repeat([]byte{0x22}, 32)
path := setupYandexPasswordDB(t, masterKey, dataKey, true,
yandexPassword{
OriginURL: "https://yandex.ru", UsernameElem: "u", UsernameVal: "alice",
PasswordElem: "p", SignonRealm: "https://yandex.ru", Password: "hunter2",
DateCreated: 13340000000000000,
},
)
got, err := extractYandexPasswords(keyretriever.MasterKeys{V10: masterKey}, path)
require.NoError(t, err)
assert.Empty(t, got, "master-password profiles should be skipped in v1")
}
func TestExtractYandexPasswords_WrongMasterKey(t *testing.T) {
goodKey := bytes.Repeat([]byte{0x11}, 32)
wrongKey := bytes.Repeat([]byte{0x99}, 32)
dataKey := bytes.Repeat([]byte{0x22}, 32)
path := setupYandexPasswordDB(t, goodKey, dataKey, false,
yandexPassword{
OriginURL: "https://yandex.ru", UsernameElem: "u", UsernameVal: "alice",
PasswordElem: "p", SignonRealm: "https://yandex.ru", Password: "hunter2",
},
)
// A wrong master key fails at the intermediate step, surfacing as an error
// from the extractor.
_, err := extractYandexPasswords(keyretriever.MasterKeys{V10: wrongKey}, path)
require.Error(t, err)
}
func TestYandexLoginAAD_NoMasterPassword(t *testing.T) {
got := yandexLoginAAD("https://example.com/", "user", "alice", "pass", "https://example.com/", nil)
h := sha1.New()
h.Write([]byte("https://example.com/\x00user\x00alice\x00pass\x00https://example.com/"))
want := h.Sum(nil)
assert.Equal(t, want, got)
assert.Len(t, got, sha1.Size)
}
func TestYandexLoginAAD_WithMasterPassword(t *testing.T) {
keyID := []byte("abc123")
got := yandexLoginAAD("u", "e1", "v1", "e2", "r", keyID)
require.Len(t, got, sha1.Size+len(keyID))
assert.Equal(t, keyID, got[sha1.Size:])
}
+16 -3
View File
@@ -94,10 +94,23 @@ func (e extensionExtractor) extract(_ keyretriever.MasterKeys, path string, data
return err
}
// yandexExtractors overrides Password extraction for Yandex,
// which uses action_url instead of origin_url.
// creditCardExtractor wraps a custom credit-card extract function, used by Yandex whose Ya Credit Cards DB stores
// rows as records(guid, public_data, private_data) with JSON blobs rather than Chromium's flat credit_cards table.
type creditCardExtractor struct {
fn func(keys keyretriever.MasterKeys, path string) ([]types.CreditCardEntry, error)
}
func (e creditCardExtractor) extract(keys keyretriever.MasterKeys, path string, data *types.BrowserData) error {
var err error
data.CreditCards, err = e.fn(keys, path)
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.Password: passwordExtractor{fn: extractYandexPasswords},
types.CreditCard: creditCardExtractor{fn: extractYandexCreditCards},
}
// operaExtractors overrides Extension extraction for Opera,
+55
View File
@@ -0,0 +1,55 @@
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) != ""
}
+208
View File
@@ -0,0 +1,208 @@
package chromium
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
"testing"
_ "modernc.org/sqlite"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// Yandex-specific SQLite schemas and test fixtures.
//
// Ya Passman Data:
// - meta(key, value) holds local_encryptor_data
// - active_keys(key_id, sealed_key) non-empty sealed_key = master password set
// - logins(...) same column set as Chromium, minus columns we
// don't query, plus a signon_realm NOT NULL
//
// Ya Credit Cards:
// - meta(key, value) holds its own local_encryptor_data
// - records(guid, public_data, private_data)
// ---------------------------------------------------------------------------
const yandexLoginsSchema = `CREATE TABLE logins (
origin_url VARCHAR NOT NULL,
action_url VARCHAR,
username_element VARCHAR,
username_value VARCHAR,
password_element VARCHAR,
password_value BLOB,
signon_realm VARCHAR NOT NULL,
date_created INTEGER NOT NULL DEFAULT 0
)`
const yandexMetaSchema = `CREATE TABLE meta (
key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY,
value LONGVARCHAR
)`
const yandexActiveKeysSchema = `CREATE TABLE active_keys (
key_id TEXT,
sealed_key TEXT
)`
const yandexRecordsSchema = `CREATE TABLE records (
guid TEXT PRIMARY KEY,
public_data TEXT,
private_data BLOB
)`
// yandexTestNonce is a fixed 12-byte nonce used across fixtures so test failures
// are easy to reproduce by hand. Real Yandex uses CSPRNG nonces per row.
var yandexTestNonce = bytes.Repeat([]byte{0x77}, 12)
// yandexMasterKeyBlobNonce is a fixed 12-byte nonce for sealing the intermediate
// data key inside meta.local_encryptor_data. Distinct from yandexTestNonce so a
// test mix-up surfaces as a decrypt error rather than a false pass.
var yandexMasterKeyBlobNonce = bytes.Repeat([]byte{0xAB}, 12)
// yandexSignatureForFixtures duplicates crypto.yandexSignature so tests can
// construct blobs without the crypto package exporting its internal constant.
// Protobuf header bytes: field1 varint=1, field2 len=32.
var yandexSignatureForFixtures = []byte{0x08, 0x01, 0x12, 0x20}
// yandexSealAESGCM seals plaintext under (key, nonce, aad) using AES-GCM.
func yandexSealAESGCM(t *testing.T, key, nonce, plaintext, aad []byte) []byte {
t.Helper()
block, err := aes.NewCipher(key)
require.NoError(t, err)
aead, err := cipher.NewGCM(block)
require.NoError(t, err)
return aead.Seal(nil, nonce, plaintext, aad)
}
// buildYandexLocalEncryptorBlob produces the exact byte layout stored in
// meta.local_encryptor_data: [preamble]"v10"[12B nonce][68B plaintext + 16B GCM tag].
// The plaintext is signature (4B) + dataKey (32B) + zero padding to 68B.
func buildYandexLocalEncryptorBlob(t *testing.T, masterKey, dataKey []byte) []byte {
t.Helper()
plaintext := append([]byte{}, yandexSignatureForFixtures...)
plaintext = append(plaintext, dataKey...)
// Pad to 68B (= 96 blob - 12 nonce - 16 tag) to match the on-disk shape.
plaintext = append(plaintext, make([]byte, 68-len(plaintext))...)
ciphertext := yandexSealAESGCM(t, masterKey, yandexMasterKeyBlobNonce, plaintext, nil)
blob := []byte{0x12, 0x34, 0x56, 0x78} // arbitrary preamble
blob = append(blob, "v10"...)
blob = append(blob, yandexMasterKeyBlobNonce...)
blob = append(blob, ciphertext...)
return blob
}
// yandexPassword describes one row of test data for the logins table.
type yandexPassword struct {
OriginURL, UsernameElem, UsernameVal, PasswordElem, SignonRealm, Password string
DateCreated int64
}
// setupYandexPasswordDB creates a Ya Passman Data SQLite file with meta,
// active_keys, and logins populated. Each logins row is sealed under dataKey
// using the same per-row AAD derivation the production extractor expects.
// Set hasMasterPassword=true to simulate a profile protected by a master
// password (a non-empty sealed_key row).
func setupYandexPasswordDB(t *testing.T, masterKey, dataKey []byte, hasMasterPassword bool, rows ...yandexPassword) string {
t.Helper()
path := filepath.Join(t.TempDir(), "Ya Passman Data")
db, err := sql.Open("sqlite", path)
require.NoError(t, err)
defer db.Close()
for _, schema := range []string{yandexLoginsSchema, yandexMetaSchema, yandexActiveKeysSchema} {
_, err = db.Exec(schema)
require.NoError(t, err)
}
blob := buildYandexLocalEncryptorBlob(t, masterKey, dataKey)
_, err = db.Exec(`INSERT INTO meta (key, value) VALUES ('local_encryptor_data', ?)`, blob)
require.NoError(t, err)
if hasMasterPassword {
_, err = db.Exec(`INSERT INTO active_keys (key_id, sealed_key) VALUES ('kid', 'sealed-opaque')`)
require.NoError(t, err)
}
for _, r := range rows {
aad := yandexLoginAAD(r.OriginURL, r.UsernameElem, r.UsernameVal, r.PasswordElem, r.SignonRealm, nil)
ciphertext := yandexSealAESGCM(t, dataKey, yandexTestNonce, []byte(r.Password), aad)
passwordBlob := append([]byte{}, yandexTestNonce...)
passwordBlob = append(passwordBlob, ciphertext...)
stmt := fmt.Sprintf(
`INSERT INTO logins (origin_url, action_url, username_element, username_value,
password_element, password_value, signon_realm, date_created)
VALUES ('%s', '', '%s', '%s', '%s', x'%s', '%s', %d)`,
r.OriginURL, r.UsernameElem, r.UsernameVal, r.PasswordElem,
hex.EncodeToString(passwordBlob), r.SignonRealm, r.DateCreated,
)
_, err = db.Exec(stmt)
require.NoError(t, err)
}
return path
}
// yandexCreditCard describes one row of test data for the records table.
type yandexCreditCard struct {
GUID string
CardHolder, CardTitle, ExpYear, ExpMonth string
FullCardNumber, PinCode, SecretComment string
}
// setupYandexCreditCardDB creates a Ya Credit Cards SQLite file with meta and
// records populated. Each record's private_data is sealed under dataKey with
// AAD = guid bytes, matching the production extractor.
func setupYandexCreditCardDB(t *testing.T, masterKey, dataKey []byte, rows ...yandexCreditCard) string {
t.Helper()
path := filepath.Join(t.TempDir(), "Ya Credit Cards")
db, err := sql.Open("sqlite", path)
require.NoError(t, err)
defer db.Close()
for _, schema := range []string{yandexRecordsSchema, yandexMetaSchema} {
_, err = db.Exec(schema)
require.NoError(t, err)
}
blob := buildYandexLocalEncryptorBlob(t, masterKey, dataKey)
_, err = db.Exec(`INSERT INTO meta (key, value) VALUES ('local_encryptor_data', ?)`, blob)
require.NoError(t, err)
for _, r := range rows {
public := yandexPublicData{
CardHolder: r.CardHolder,
CardTitle: r.CardTitle,
ExpireDateYear: r.ExpYear,
ExpireDateMonth: r.ExpMonth,
}
publicJSON, err := json.Marshal(public)
require.NoError(t, err)
private := yandexPrivateData{
FullCardNumber: r.FullCardNumber,
PinCode: r.PinCode,
SecretComment: r.SecretComment,
}
privateJSON, err := json.Marshal(private)
require.NoError(t, err)
aad := yandexCardAAD(r.GUID, nil)
ciphertext := yandexSealAESGCM(t, dataKey, yandexTestNonce, privateJSON, aad)
privateBlob := append([]byte{}, yandexTestNonce...)
privateBlob = append(privateBlob, ciphertext...)
_, err = db.Exec(
`INSERT INTO records (guid, public_data, private_data) VALUES (?, ?, ?)`,
r.GUID, string(publicJSON), privateBlob,
)
require.NoError(t, err)
}
return path
}