Skip to content

fix(auth-state): restore AppStateSyncKeyData.fromObject for app-state sync keys - #2685

Open
tgiorgio wants to merge 1 commit into
evolution-foundation:developfrom
tgiorgio:fix/appstate-sync-key-fromobject
Open

fix(auth-state): restore AppStateSyncKeyData.fromObject for app-state sync keys#2685
tgiorgio wants to merge 1 commit into
evolution-foundation:developfrom
tgiorgio:fix/appstate-sync-key-fromobject

Conversation

@tgiorgio

@tgiorgio tgiorgio commented Aug 12, 2026

Copy link
Copy Markdown

Problem

The three auth-state providers read the persisted app-state sync key with proto.Message.AppStateSyncKeyData.create(value):

  • src/utils/use-multi-file-auth-state-prisma.ts:185
  • src/utils/use-multi-file-auth-state-redis-db.ts:64
  • src/utils/use-multi-file-auth-state-provider-files.ts:119

create() is a bare constructor: it copies own properties and performs no type coercion. fromObject() is the one that converts, and for bytes fields it base64-decodes a string into a Uint8Array. Baileys' own reference implementation uses fromObject() for exactly this reason (Utils/use-multi-file-auth-state.ts).

The value is stored as base64 text because protobufjs Message.toJSON() uses util.toJSONOptions with bytes: String, so the Uint8Array is already a string by the time BufferJSON.replacer runs. Reading it back with create() leaves it a string, and the round trip is asymmetric.

Consequence

hkdf() receives a string and evaluates new Uint8Array("7ae+...")ToIndex(NaN)length 0. Every mutation key is derived from empty key material:

Invalid patch mac
failed to sync state from version, removing and trying from scratch
error:1C800064:Provider routines::bad decrypt
resyncing regular from v0        <- loops forever

It looks fine for the first few minutes after pairing because makeCacheableSignalKeyStore serves the original Buffer-bearing object from its NodeCache (SIGNAL_STORE, 5 min TTL). After expiry the cold read returns the corrupt string — and writes it back into the cache, so it never recovers on its own.

The whole regular collection is affected. The way it usually surfaces: chat labels applied on the phone stop reaching the webhook, labels.association fires during pairing and never again.

A useful diagnostic: a healthy instance persists app-state-sync-version-* for all five collections; a broken one only ever has critical_block.

Fix

create(fromObject( at the three call sites. grep -rn AppStateSyncKeyData src/ returns exactly those three hits.

Regression origin

Introduced in 8830f47 (bump to v2.3.3). v2.3.2 is clean; v2.3.3 through v2.3.7, main and develop all carry it. The three files are byte-identical across those refs, so this patch applies unchanged to all of them.

Verification

Reproduced and fixed on a real 2.3.7 instance (Baileys 7.0.0-rc.9), with both the Redis and the file storage backends — they fail identically, which is what pointed above the storage layer in the first place.

Empirically, on the same instance:

create()     -> keyData is a 44-char String
fromObject() -> keyData is the correct 32-byte Buffer

After the change, the instance resynced regular from v0 to v19 with zero decrypt errors and label events resumed immediately. Keys already persisted in the broken format are recovered as-is — no re-pairing is required, which matters for anyone who has been running a broken instance for a while.

Note

This is the same fix as #2593, which was closed for targeting main. This one targets develop per CONTRIBUTING.

🤖 Generated with Claude Code

Summary by Sourcery

Bug Fixes:

  • Fix app-state sync key deserialization by using the protobuf fromObject API so persisted keys are loaded as binary data rather than base64 strings, preventing broken HKDF derivation and state sync failures across Prisma, file, and Redis auth-state backends.

… sync keys

The three auth-state providers deserialise the persisted app-state sync key
with `proto.Message.AppStateSyncKeyData.create(value)`. `create()` is a bare
constructor that copies own properties with no type coercion, so `keyData`
stays the base64 **string** it was serialised as, instead of becoming bytes.

Serialisation turns it into a string because protobufjs `Message.toJSON()`
uses `util.toJSONOptions` (`bytes: String`), which converts the `Uint8Array`
to base64 before `BufferJSON.replacer` ever sees the value. Baileys' own
reference implementation therefore reads it back with `.fromObject()`, which
base64-decodes `bytes` fields (see `Utils/use-multi-file-auth-state.ts`).

With a string, `hkdf()` evaluates `new Uint8Array("7ae+...")` -> ToIndex(NaN)
-> length 0, so every mutation key is derived from EMPTY key material. The
observable result is app-state sync failing permanently:

    Invalid patch mac
    failed to sync state from version, removing and trying from scratch
    error:1C800064:Provider routines::bad decrypt
    resyncing regular from v0   (looping forever)

It is invisible for the first few minutes after pairing, because
`makeCacheableSignalKeyStore` serves the original Buffer-bearing object from
its NodeCache (`SIGNAL_STORE`, 5 min TTL). Once that entry expires the cold
read returns the corrupt string, and the corrupt value is written back into
the cache, so it never recovers.

Everything carried by the `regular` collection is lost, which is how this
surfaces in practice: chat labels applied on the phone never reach the
webhook, `labels.association` stops firing entirely after pairing.

Introduced in 8830f47 (v2.3.3); 2.3.2 is
clean. Present on 2.3.3 through 2.3.7, main and develop.

Verified on 2.3.7 with a real instance: after the change the same instance
resynced `regular` from v0 to v19 with zero decrypt errors, and label events
started flowing again. Keys already persisted in the broken form are
recovered as-is, so no re-pairing is required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Switches app-state sync key deserialization from a raw constructor to protobuf fromObject() in all three auth-state providers so persisted keys are correctly decoded from base64 into byte buffers and state sync resumes working reliably.

Sequence diagram for app-state sync key deserialization using fromObject

sequenceDiagram
  participant AuthStateProvider
  participant Storage as readData
  participant Proto as AppStateSyncKeyData
  participant Crypto as hkdf

  AuthStateProvider->>Storage: readData(app-state-sync-key-id)
  Storage-->>AuthStateProvider: value (base64 string)
  AuthStateProvider->>Proto: fromObject(value)
  Proto-->>AuthStateProvider: keyData (Uint8Array)
  AuthStateProvider->>Crypto: hkdf(keyData)
Loading

File-Level Changes

Change Details Files
Fix app-state sync key deserialization to use protobuf fromObject() for proper bytes decoding and symmetric round-tripping across all auth-state providers.
  • Replace AppStateSyncKeyData.create(value) with AppStateSyncKeyData.fromObject(value) when reading persisted app-state-sync-key records.
  • Apply the same deserialization fix consistently in the Prisma, Redis DB, and provider-files multi-file auth-state helpers.
  • Ensure existing stored keys in base64 string form are correctly recovered into Buffer/Uint8Array without requiring re-pairing.
src/utils/use-multi-file-auth-state-prisma.ts
src/utils/use-multi-file-auth-state-provider-files.ts
src/utils/use-multi-file-auth-state-redis-db.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant