Files

209 lines
7.4 KiB
Go

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
}