Back to Hackbrowserdata

RFC-012: Yandex Browser Decryption

rfcs/012-yandex-decryption.md

1.0.011.3 KB
Original Source

RFC-012: Yandex Browser Decryption

Author: moonD4rk Status: Living Document Created: 2026-04-22 Last updated: 2026-04-22

1. Overview

Yandex Browser is a Chromium fork, but its saved-credential encryption diverges from the Chromium reference in three ways that together make a plain Chromium extractor produce zero plaintext:

  1. The Chromium master key (DPAPI on Windows, Keychain on macOS) does not decrypt password_value directly — it decrypts a per-DB intermediate key stored in meta.local_encryptor_data. That intermediate key is what actually decrypts rows.
  2. Each row's AES-GCM ciphertext is sealed with row-specific Additional Authenticated Data (AAD). A password row's AAD is a SHA-1 digest over five form fields joined by \x00; a credit-card row's AAD is the row's guid. AAD mismatch → GCM tag failure → empty plaintext.
  3. Credit cards live in records(guid, public_data, private_data) — two JSON blobs — not Chromium's flat credit_cards table.

This RFC documents the on-disk layout, the decryption math, and how the integration plugs into the existing Chromium extract pipeline without perturbing the v10/v11/v20 paths that the rest of HackBrowserData depends on.

Resolved issues: #90 (feature request), #105 / #462 / #476 (downstream bug reports against the incomplete skeleton that was merged before this RFC).

Related RFCs:

  • RFC-003 — Chromium cipher versions (v10 / v11 / v20)
  • RFC-006 — master-key retrieval chain

Deferred to a follow-up RFC / PR:

  • Master-password (RSA-OAEP + PBKDF2) unseal path.
  • Windows ABE v20 for Yandex — not in scope until Yandex adopts App-Bound Encryption.
  • Linux support; Yandex Browser has no official Linux build.

2. Protocol differences at a glance

LayerStandard ChromiumYandex
Master keyos_crypt.encrypted_key in Local State, unwrapped via DPAPI / KeychainSame
Decryption key used per rowMaster key directlyIntermediate 32-byte key stored per-DB in meta.local_encryptor_data
Key wrapper format"v10"|nonce|ct+tag (or DPAPI blob)"v10"|nonce|ct+tag, plaintext prefixed by 4B protobuf signature 08 01 12 20, 32B key follows
Password DB fileLogin Data (table: logins)Ya Passman Data (table: logins)
Password ciphertext"v10"|nonce|ct+tag, AAD = emptyNo prefix; raw nonce|ct+tag; AAD = SHA1(origin_url ‖ \x00 ‖ username_element ‖ \x00 ‖ username_value ‖ \x00 ‖ password_element ‖ \x00 ‖ signon_realm)
Credit-card DB fileWeb Data (table: credit_cards)Ya Credit Cards (table: records)
Credit-card layoutColumns: name_on_card, expiration_month, card_number_encrypted, …JSON: public_data (plaintext) + private_data (AES-GCM sealed JSON, AAD = guid)
Master passwordn/aOptional; when set, active_keys.sealed_key holds an RSA-OAEP envelope (deferred)

3. On-disk layout

3.1 meta.local_encryptor_data

[protobuf preamble bytes...] "v10" [12B nonce] [68B plaintext + 16B GCM tag]

The 68-byte plaintext (decrypted with the Chromium master key, empty AAD) has the shape:

08 01 12 20  | KK KK ... KK  (32 bytes)  | padding / extra protobuf fields
^ signature  | ^ data-encryption key

The data-encryption key is the first 32 bytes after the signature; trailing bytes are ignored. The fixed 96-byte region after "v10" is a Yandex invariant (the reference implementation slices [:96] unconditionally) and is checked as a minimum length.

3.2 Password row (logins.password_value)

[12B nonce] [ciphertext] [16B GCM tag]

No version prefix. AAD binds five form columns:

SHA1(origin_url ‖ 0x00 ‖ username_element ‖ 0x00 ‖ username_value ‖ 0x00 ‖ password_element ‖ 0x00 ‖ signon_realm)

When a master password is set, the sealed keyID is appended after the SHA-1 sum. v1 always passes nil and skips sealed profiles.

3.3 Credit card row (records.private_data)

Same byte shape as passwords but AAD = the row's guid bytes (plus optional keyID). Decrypted plaintext is a JSON object with full_card_number, pin_code, secret_comment. The sibling public_data column is plaintext JSON with card_holder, card_title, expire_date_month, expire_date_year.

4. Architecture

4.1 Two-level key hierarchy

Yandex adds a second key layer on top of the standard Chromium key. The Chromium master key — unwrapped from Local State via DPAPI (Windows) or Keychain (macOS) — never decrypts row ciphertext directly. Instead, each target SQLite database carries its own data key in meta.local_encryptor_data, and only that data key decrypts row-level ciphertext. The master key's only job is to unwrap the data key.

4.2 Recovery steps

For every target DB (Ya Passman Data for passwords, Ya Credit Cards for cards), the extractor runs the same five steps:

  1. Master key: read Local State, base64-decode os_crypt.encrypted_key, strip the DPAPI prefix, and unwrap it via DPAPI (Windows) or Keychain (macOS). Yields 32 bytes.
  2. Open DB: open the target SQLite file (a temp copy is used to avoid lock contention if the browser is running).
  3. Master-password gate: SELECT sealed_key FROM active_keys. Non-empty → log a warning and skip the profile (v1 limitation — RSA-OAEP unseal deferred). 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], AES-GCM-decrypt with the data key under that AAD.

4.3 Key hierarchy

LevelKeyOriginScope
1Chromium master keyLocal State → DPAPI / KeychainWhole profile (shared with cookies, history, etc.)
2aPasswords data keyYa Passman Datameta.local_encryptor_datalogins rows in this DB only
2bCredit cards data keyYa Credit Cardsmeta.local_encryptor_datarecords rows in this DB only

4.4 Per-category decryption inputs

CategoryDB fileTable / columnCiphertext layoutAAD
PasswordYa Passman Datalogins.password_value[12B nonce][ct+tag]SHA1(origin_url ‖ \x00 ‖ username_element ‖ \x00 ‖ username_value ‖ \x00 ‖ password_element ‖ \x00 ‖ signon_realm)
Credit cardYa Credit Cardsrecords.private_data[12B nonce][ct+tag]raw guid bytes

Credit-card plaintext is a JSON object (full_card_number, pin_code, secret_comment) that the extractor unmarshals into CreditCardEntry. The sibling records.public_data is plaintext JSON (card_holder, card_title, expire_date_year, expire_date_month) and needs no decryption.

4.5 Independence property

The two level-2 data keys are unwrapped from different meta.local_encryptor_data blobs — one per DB. This matters in two ways:

  • A profile with a master password blocks passwords (step 3 trips) but credit cards can still decrypt, because the card DB has no active_keys table.
  • Corruption of one DB's meta blob does not cascade to the other.

Both data keys still ultimately derive from the same level-1 Chromium master key, so loss of DPAPI (e.g., Windows user-profile rebuild) breaks both simultaneously.

5. Layering rationale

5.1 Yandex-specific derivation stays in the extract path, not the key-retrieval layer

The key-retrieval layer dispatches on cipher-version prefix — v10 / v11 / v20. Yandex password rows carry no such prefix; they are raw [nonce][ct+tag]. Folding Yandex's intermediate-key step into the prefix dispatcher would overload an abstraction that is purely "pick the key for this byte prefix". The intermediate-key unwrap therefore lives alongside the Yandex extractor and consumes the standard Chromium master key as input; the prefix dispatcher is untouched.

5.2 AAD construction belongs with the consumer, not the crypto layer

The crypto layer exposes cryptographic primitives — transforms of bytes under a key (AES, GCM, 3DES, DPAPI, PBKDF2). Yandex's AAD rules (SHA-1 over five form fields for passwords, the row's GUID for cards) are not cryptography; they are Yandex's per-row identification scheme that happens to be bound to GCM's authentication tag. Placing them in the crypto layer would leak product-specific knowledge into a layer that otherwise sees only bytes and keys.

The final split:

  • A single generic AES-GCM-with-AAD primitive in the crypto layer. Any current or future protocol that needs per-row AAD can reuse it without the crypto layer growing per-product surface.
  • Yandex-specific AAD helpers next to the consumer that builds the AAD inputs. Product knowledge stays with the product.

This keeps the crypto surface minimal — the only Yandex symbol it owns is the intermediate-key unwrap, because that one function genuinely is cryptography (it strips a protobuf frame and decrypts AES-GCM).

6. Non-goals and deferred work

  1. Master-password unseal (#90 edge case). Profiles with a non-empty active_keys.sealed_key are detected and skipped with a warning. A follow-up RFC will cover the RSA-OAEP path: PBKDF2-SHA256 derives a KEK; the KEK decrypts encrypted_private_key with AAD = unlock_key_salt; the resulting PKCS8 RSA private key + RSA-OAEP-SHA256 decrypts encrypted_encryption_key; the signature strip then yields the dataKey.
  2. Windows ABE v20 for Yandex. Yandex has not adopted App-Bound Encryption. If that changes, Yandex joins the RFC-010 vendor table and the ABE path begins returning a non-empty v20 key for Yandex ciphertexts.
  3. Linux support. Yandex Browser has no official Linux release, so there is no Linux code path to add.

7. Test strategy

Decryption math is covered by cross-platform unit tests that build synthetic DBs by running the encryption path in reverse — no real Yandex install or Windows host is required. Coverage spans:

  • Intermediate-key unwrap: round-trip, missing v10 marker, truncated blob, bad protobuf signature, trailing bytes ignored.
  • AES-GCM-with-AAD primitive: round-trip, mismatched AAD surfaces as authentication failure, under-sized blob surfaces as a distinct error.
  • Password extraction: round-trip on multi-row fixtures, master-password skip path, wrong master key surfaces as error.
  • Credit-card extraction: round-trip on multi-card fixtures verifying every JSON field maps to the output schema; count; wrong master key surfaces as error.
  • AAD formulas: SHA-1 field concatenation (passwords), GUID bytes (cards), both with and without a master-password keyID appended.

End-to-end validation on a Windows host with a real Yandex profile is expected before shipping changes that touch the decryption path; the existing Chromium full-sweep doubles as a regression gate against unintended impact on other Chromium forks.

8. Rollout

Single PR that wires all of the above; merge automatically closes #90 / #105 / #462 / #476. Follow-up PRs for master password and (if/when Yandex adopts ABE) v20 integration reference this RFC rather than reopening the decryption design question.