feat(yandex): full password and credit card decryption#585
Conversation
Implement Yandex's decryption protocol end-to-end. The existing skeleton queried the wrong URL column (action_url instead of origin_url), so the per-row AAD never matched the one Yandex seals GCM with, and every row decrypted to empty plaintext — that's why #105, #462, #476 kept surfacing against the "supported" browser. Each Ya Passman Data / Ya Credit Cards DB holds a per-DB data key inside meta.local_encryptor_data: a protobuf-framed 96-byte blob encrypted under the Chromium master key. The row-level data (password_value, records.private_data) is AES-GCM with per-row AAD — SHA1 over five form fields for passwords, the row's guid for cards. Credit cards live in records(guid, public_data, private_data) as two JSON blobs, not Chromium's credit_cards table, so CreditCardEntry gains optional CVC and Comment fields. Profiles guarded by a browser-level master password (non-empty active_keys.sealed_key) are detected and skipped with a warning; RSA-OAEP unseal is deferred. Linux is out of scope: Yandex Browser has no Linux release. Validated on a Windows 10 sandbox with a real Yandex profile: two stored passwords decrypt to readable ASCII (stackoverflow and douban); full-sweep regression across 13 browsers is unchanged (703 cookies, 0 non-ASCII). Design documented in RFC-012. Closes #90 Closes #105 Closes #462 Closes #476
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #585 +/- ##
==========================================
- Coverage 73.59% 70.89% -2.71%
==========================================
Files 59 61 +2
Lines 2651 3274 +623
==========================================
+ Hits 1951 2321 +370
- Misses 524 761 +237
- Partials 176 192 +16
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds full Yandex Browser support for decrypting saved passwords and credit cards within the existing Chromium extraction pipeline, implementing Yandex’s two-level key hierarchy (Chromium master key → per-DB data key in meta.local_encryptor_data) and row-level AES-GCM AAD rules.
Changes:
- Implement Yandex per-DB data-key unwrapping (
meta.local_encryptor_data) plus AES-GCM row decryption using per-row AAD for passwords and credit cards. - Add Yandex credit-card extraction from
records(guid, public_data, private_data)and route counting/extraction based onChromiumYandex. - Extend
types.CreditCardEntrywithCVCandComment, and add unit/integration-style tests + an RFC describing the design.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
types/models.go |
Extends CreditCardEntry with CVC and Comment fields for Yandex output. |
rfcs/012-yandex-decryption.md |
Adds RFC documenting Yandex decryption design and integration points. |
output/reflect_test.go |
Updates CSV header expectations for the new credit-card fields. |
crypto/crypto.go |
Adds AESGCMDecryptBlob(key, blob, aad) primitive for nonce-prefixed AES-GCM blobs. |
crypto/yandex.go |
Adds Yandex intermediate-key unwrap (DecryptYandexIntermediateKey). |
crypto/yandex_test.go |
Adds unit tests for Yandex intermediate key unwrap and AESGCMDecryptBlob. |
crypto/crypto_windows.go |
Removes obsolete DecryptYandex Windows-only stub. |
browser/chromium/yandex_key.go |
Implements per-DB Yandex data-key loading + master-password gate detection. |
browser/chromium/extract_password.go |
Implements Yandex password extraction with SHA1-based AAD and row decrypt. |
browser/chromium/extract_creditcard.go |
Implements Yandex credit-card extraction from records JSON blobs + guid AAD decrypt. |
browser/chromium/source.go |
Registers Yandex extractors for Password + CreditCard via a new creditCardExtractor. |
browser/chromium/chromium.go |
Routes Yandex credit-card counting to records-based counter. |
browser/chromium/yandex_testutil_test.go |
Adds Yandex-specific SQLite fixture builders for passwords/cards. |
browser/chromium/extract_password_test.go |
Adds end-to-end Yandex password tests incl. master-password skip and AAD unit tests. |
browser/chromium/extract_creditcard_test.go |
Adds end-to-end Yandex credit-card tests incl. count and guid-AAD unit test. |
browser/chromium/chromium_test.go |
Ensures Yandex extractor map includes CreditCard override. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 2. **Open DB**: `session.Acquire` has already copied the target SQLite file to a temp path; `loadYandexDataKey` opens it there. | ||
| 3. **Master-password gate**: `SELECT sealed_key FROM active_keys`. Non-empty → return `errYandexMasterPasswordSet`; the caller logs a warning and skips the profile (v1 limitation). Table missing (credit-card DB) or empty value → continue. | ||
| 4. **Data key**: `SELECT value FROM meta WHERE key='local_encryptor_data'`. Find the `"v10"` byte sequence, take the 96 bytes that follow, split into 12B nonce + 84B (ciphertext+tag). AES-GCM-decrypt with the master key (no AAD). Strip the 4-byte protobuf signature `08 01 12 20`. Keep the first 32 bytes. | ||
| 5. **Per-row decryption**: for each row, compute AAD (see §4.4), split `[12B nonce][ct+tag]`, call `AESGCMDecryptWithAAD(dataKey, nonce, ct, aad)`. |
There was a problem hiding this comment.
The RFC refers to AESGCMDecryptWithAAD(...) for per-row decryption, but the implementation uses crypto.AESGCMDecryptBlob(key, blob, aad) and there is no AESGCMDecryptWithAAD symbol. Please update the RFC to match the actual function/API names so readers can follow the code path accurately.
| 5. **Per-row decryption**: for each row, compute AAD (see §4.4), split `[12B nonce][ct+tag]`, call `AESGCMDecryptWithAAD(dataKey, nonce, ct, aad)`. | |
| 5. **Per-row decryption**: for each row, compute AAD (see §4.4). The encrypted row value is a blob laid out as `[12B nonce][ct+tag]`; the implementation passes that blob directly to `crypto.AESGCMDecryptBlob(dataKey, blob, aad)`. |
| | `crypto/yandex.go` | Pure-Go primitives: `DecryptYandexIntermediateKey`, `AESGCMDecryptWithAAD`, `YandexLoginAAD`, `YandexCardAAD`, `YandexSignature`. Cross-platform, unit-testable on any host. | | ||
| | `crypto/yandex_test.go` | Round-trip tests using synthesized blobs (no Yandex install required). | | ||
| | `browser/chromium/yandex_key.go` | `loadYandexDataKey(dbPath, masterKey)` — opens the DB, checks `active_keys`, reads `meta.local_encryptor_data`, returns dataKey or `errYandexMasterPasswordSet`. | | ||
| | `browser/chromium/extract_password.go` | `extractYandexPasswords` — queries `origin_url, username_element, username_value, password_element, password_value, signon_realm, date_created`; computes per-row AAD; decrypts. | | ||
| | `browser/chromium/extract_creditcard_yandex.go` | `extractYandexCreditCards` + `countYandexCreditCards` — queries `records`; decrypts `private_data` with guid-AAD; parses both JSON blobs. | |
There was a problem hiding this comment.
In the “Code layout” table, crypto/yandex.go is listed as containing AESGCMDecryptWithAAD, YandexLoginAAD, YandexCardAAD, and YandexSignature, but those functions/constants aren’t present there (AAD helpers live under browser/chromium; the generic primitive is crypto.AESGCMDecryptBlob in crypto/crypto.go; signature is unexported). Please reconcile this section (and §5.2 / test-strategy mentions) with the current code organization.
| | `crypto/yandex.go` | Pure-Go primitives: `DecryptYandexIntermediateKey`, `AESGCMDecryptWithAAD`, `YandexLoginAAD`, `YandexCardAAD`, `YandexSignature`. Cross-platform, unit-testable on any host. | | |
| | `crypto/yandex_test.go` | Round-trip tests using synthesized blobs (no Yandex install required). | | |
| | `browser/chromium/yandex_key.go` | `loadYandexDataKey(dbPath, masterKey)` — opens the DB, checks `active_keys`, reads `meta.local_encryptor_data`, returns dataKey or `errYandexMasterPasswordSet`. | | |
| | `browser/chromium/extract_password.go` | `extractYandexPasswords` — queries `origin_url, username_element, username_value, password_element, password_value, signon_realm, date_created`; computes per-row AAD; decrypts. | | |
| | `browser/chromium/extract_creditcard_yandex.go` | `extractYandexCreditCards` + `countYandexCreditCards` — queries `records`; decrypts `private_data` with guid-AAD; parses both JSON blobs. | | |
| | `crypto/yandex.go` | Yandex-specific key-unwrapping logic, including `DecryptYandexIntermediateKey`. Pure Go and unit-testable on any host. | | |
| | `crypto/crypto.go` | Generic AES-GCM primitive: `AESGCMDecryptBlob`. Shared helper used by browser-specific extractors once they have derived the correct row AAD. | | |
| | `browser/chromium/yandex_key.go` | `loadYandexDataKey(dbPath, masterKey)` — opens the DB, checks `active_keys`, reads `meta.local_encryptor_data`, returns dataKey or `errYandexMasterPasswordSet`. | | |
| | `browser/chromium/extract_password.go` | `extractYandexPasswords` — queries `origin_url, username_element, username_value, password_element, password_value, signon_realm, date_created`; derives the password-row AAD from those fields; decrypts via the generic AES-GCM helper. | | |
| | `browser/chromium/extract_creditcard_yandex.go` | `extractYandexCreditCards` + `countYandexCreditCards` — queries `records`; uses `guid` as the card-row AAD when decrypting `private_data`; parses both JSON blobs. | |
| // AESGCMDecryptBlob decrypts a blob shaped as [12B nonce][ciphertext+16B GCM tag] with caller-supplied AAD. | ||
| // Used by protocols that wrap AES-GCM output with a fixed-length nonce prefix (Yandex passwords/cards). | ||
| func AESGCMDecryptBlob(key, blob, aad []byte) ([]byte, error) { | ||
| if len(blob) < gcmNonceSize { |
There was a problem hiding this comment.
AESGCMDecryptBlob documents the blob as [12B nonce][ciphertext+16B GCM tag], but it only checks len(blob) < gcmNonceSize. For blobs that include a nonce but are missing the tag/ciphertext, callers will get a lower-level GCM error instead of the package’s sentinel errShortCiphertext. Consider validating len(blob) >= gcmNonceSize + 16 (and possibly > gcmNonceSize) before calling Open so short inputs are classified consistently.
| if len(blob) < gcmNonceSize { | |
| if len(blob) < gcmNonceSize+16 { |
| // 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) != "" |
There was a problem hiding this comment.
hasMasterPassword currently treats any query error as “no master password”. That means real DB issues (corruption, missing column, unexpected schema changes) will be silently ignored and the extractor will proceed, likely producing confusing decrypt failures. Consider returning (bool, error) and only treating “no such table” / sql.ErrNoRows as false; propagate other errors up via loadYandexDataKey so the caller can surface them.
Align the RFC with the code that actually shipped: - §5 Code layout: drop stale references to AESGCMDecryptWithAAD / YandexLoginAAD / YandexCardAAD / YandexSignature (unexported or moved); add the new crypto.AESGCMDecryptBlob generic primitive; note that extract_creditcard_yandex.go was merged back into extract_creditcard.go; mention the local yandexLoginAAD / yandexCardAAD helpers that now live next to their extractors. - §5.2: replace the "why AESGCMDecryptWithAAD is new" rationale with the final split — generic AES-GCM + AAD primitive in crypto, Yandex-specific AAD construction in chromium. - §7 Test strategy: update the file/test inventory, point at the merged extract_creditcard_test.go, and soften the regression baseline wording to "0 non-ASCII" instead of a stale 574-cookie number.
RFCs document the technical design; per-maintainer sandbox state (specific browser versions last verified, private playbook references, timestamped "tested against Chrome 147" status markers, cookie-count baselines tied to a single contributor's host) doesn't belong in them. - RFC-010 §1.1: replace "Tested matrix (as of …)" with a compatibility- contract table; drop the "Last verified" column and the reference to the author's private regression playbook. - RFC-010 §10: drop "tested against Chrome 147 family" timestamp, "✅ verified" / "not yet sandbox-tested" status markers; keep the technical behavior columns. - RFC-010 §11-12: drop "private maintainer notes" pointer and the "not observed on Chrome 147 sandbox outputs" / "no observed conflict" lines; restate the unknowns as open questions. - RFC-012 §6: drop "as of 2026-04" timestamp on the Yandex-ABE note. - RFC-012 §7: replace the CLAUDE.local.md / 574-cookie validation recipe with a one-line general statement about the full-sweep regression gate.
RFCs describe the technical design — the Yandex protocol, key hierarchy, AAD formulas, on-disk byte layout. Go-specific implementation (which function lives in which file, which exact signature, which SQL column list) is subject to refactor and doesn't belong here. - §4.2: drop references to specific Go symbols (loadYandexDataKey, AESGCMDecryptWithAAD) from the recovery-steps list; describe each step by what it does, not by which function does it. - §5: replace the "Code layout" file-path table with a layering-rationale section. Keep §5.1 (why Yandex derivation stays in the extract path) and §5.2 (why AAD construction is not in the crypto layer), both rephrased without Go package paths or exported-symbol names. - §6: drop file-path pointers (crypto/windows/abe_native/com_iid.c, browser/browser_linux.go, ABERetriever); describe the future paths by their effect on the behavior table. - §7: replace the test-file breakdown table with a coverage summary organized by scenario (intermediate-key unwrap, AES-GCM-with-AAD, password/card extraction, AAD formulas), not by file name.
Summary
meta.local_encryptor_data(96-byte AES-GCM blob + protobuf signature strip), and the data key decrypts row-level ciphertext.\x00. Credit cards use AAD = rowguidand store data as two JSON blobs inrecords(guid, public_data, private_data)rather than Chromium's flatcredit_cardstable.types.CreditCardEntrygains optionalCVC/Commentfields (empty for Chromium; filled for Yandex).action_urlinstead oforigin_url, so the AAD never matched and every row decrypted to empty. That's the root cause behind yandex passwords are not output (on windows 11) #105 / Doesn't work on Yandex browser #462 / Can't dencrypt password in yandex browser #476 — they all surfaced the same gap under different browser versions.crypto.AESGCMDecryptBlob(key, blob, aad)splits[12B nonce][ct+tag]and decrypts. Generic, not Yandex-specific.Deferred (follow-up PRs):
active_keys.sealed_keyare detected and skipped with a warning in v1.browser_linux.goentry.Package layering
cryptoonly exposes pure crypto operations:AESGCMDecryptBlob(generic primitive) andDecryptYandexIntermediateKey(Yandex-specific because of the protobuf signature strip).browser/chromiumowns Yandex protocol knowledge:yandexLoginAADlives next to the password extractor;yandexCardAADlives next to the credit-card extractor. AAD construction is protocol logic, not crypto.Test plan
Closes #90
Closes #105
Closes #462
Closes #476