Skip to content

fix: use AppStateSyncKeyData.fromObject instead of .create for app-state keys#2593

Open
ropic wants to merge 1 commit into
evolution-foundation:mainfrom
ropic:fix/app-state-sync-key-fromobject
Open

fix: use AppStateSyncKeyData.fromObject instead of .create for app-state keys#2593
ropic wants to merge 1 commit into
evolution-foundation:mainfrom
ropic:fix/app-state-sync-key-fromobject

Conversation

@ropic

@ropic ropic commented Jun 19, 2026

Copy link
Copy Markdown

Problem

All three auth-state providers (Prisma, Redis, file-based) read app-state-sync-key data using:

value = proto.Message.AppStateSyncKeyData.create(value);

This causes error:1C800064:Provider routines::bad decrypt on every attempt to decode app-state mutations. As a result, no app-state events fire (labels.association, chats.update from archives, mutes, etc.).

Root Cause

The write path stores keys with JSON.stringify(value, BufferJSON.replacer) — Baileys' serializer that encodes Buffer/Uint8Array fields as {"type":"Buffer","data":"<base64>"}.

The read path parses them with JSON.parse(stored, BufferJSON.reviver) — which correctly reconstructs Buffer objects.

However, after reviver runs, the code wraps the result with .create(value). The protobuf .create() method performs no type conversion — it copies fields as-is. It does not coerce a restored Buffer to the exact Uint8Array layout that the proto field expects for all downstream operations. This produces a keyData buffer that is wrong for HKDF key derivation, leading to a wrong AES-256-CBC key and the bad decrypt error.

.fromObject(value), on the other hand, is specifically designed to accept a plain JS object and convert all fields to their correct proto types — which is exactly what we need after JSON deserialization.

Fix

In all three auth-state files, change .create to .fromObject:

  if (type === 'app-state-sync-key' && value) {
-   value = proto.Message.AppStateSyncKeyData.create(value);
+   value = proto.Message.AppStateSyncKeyData.fromObject(value);
  }

Files changed:

  • 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

Verified Behavior

After this fix:

  • App-state decryption succeeds (no more bad decrypt)
  • Full snapshot sync processes correctly (363 mutations for the regular collection on first connect)
  • Delta syncs work (only new patches processed on subsequent changes)
  • labels.association, chats.update, and other app-state events fire correctly

Tested with Evolution API v2.3.7, Baileys 7.0.0-rc.9, PostgreSQL auth-state provider.

The fix is identical and necessary for all three providers.

Summary by Sourcery

Bug Fixes:

  • Correct app-state sync key reconstruction by using the protobuf fromObject helper instead of create in Prisma, file-based, and Redis auth-state providers so app-state decryption and events work as expected.

…ate keys

When reading app-state-sync-key data from any auth-state provider
(Prisma, Redis, files), the stored JSON is deserialized with
BufferJSON.reviver which correctly restores Buffer objects. However,
wrapping the result with AppStateSyncKeyData.create() does not coerce
the restored Buffer fields to the Uint8Array types that the proto
library expects for all downstream operations.

This produces incorrect keyData in the HKDF derivation step, which
in turn generates a wrong AES-256-CBC key and causes
"error:1C800064:Provider routines::bad decrypt" on every attempt to
decode app-state mutations (labels, archives, mutes, etc.).

AppStateSyncKeyData.fromObject() performs full type conversion of
all fields from a plain JS object, which is exactly what is needed
after JSON deserialization.

Affects all three auth-state providers: Prisma, Redis, and file-based.
Tested with the Prisma provider — no app-state events fired before
this fix; all events (labels.association, chats.update, etc.) fire
correctly after.
@sourcery-ai

sourcery-ai Bot commented Jun 19, 2026

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

Reviewer's Guide

Replaces protobuf .create() with .fromObject() when reading app-state-sync-key values across all three auth-state providers so that JSON-deserialized key objects are correctly converted into proto types, fixing app-state decryption and event emission.

Sequence diagram for reading app-state-sync-key with fromObject instead of create

sequenceDiagram
  participant AuthStateProvider
  participant Storage
  participant BufferJSON
  participant AppStateSyncKeyData

  AuthStateProvider->>Storage: readData(type_id)
  Storage-->>AuthStateProvider: value_json
  AuthStateProvider->>BufferJSON: reviver(value_json)
  BufferJSON-->>AuthStateProvider: value_object

  alt using_create
    AuthStateProvider->>AppStateSyncKeyData: create(value_object)
    AppStateSyncKeyData-->>AuthStateProvider: keyData_invalid
    Note over AuthStateProvider: [bad decrypt, no app-state events]
  else using_fromObject
    AuthStateProvider->>AppStateSyncKeyData: fromObject(value_object)
    AppStateSyncKeyData-->>AuthStateProvider: keyData_valid
    Note over AuthStateProvider: [decryption ok, events emitted]
  end
Loading

File-Level Changes

Change Details Files
Use proto.Message.AppStateSyncKeyData.fromObject instead of .create when hydrating app-state-sync-key data after JSON deserialization.
  • Update the app-state-sync-key read path in the Prisma-backed auth-state utility to call AppStateSyncKeyData.fromObject on the loaded value before use.
  • Update the app-state-sync-key read path in the file-based auth-state provider to call AppStateSyncKeyData.fromObject on the loaded value before use.
  • Update the app-state-sync-key read path in the Redis-backed auth-state utility to call AppStateSyncKeyData.fromObject on the loaded value before use.
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 found 1 issue, and left some high level feedback:

  • Since the app-state-sync-key handling logic is now identical across the three auth-state providers, consider extracting this into a shared helper (e.g., normalizeAppStateSyncKeyData) to avoid duplication and keep future changes to this behavior centralized.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Since the `app-state-sync-key` handling logic is now identical across the three auth-state providers, consider extracting this into a shared helper (e.g., `normalizeAppStateSyncKeyData`) to avoid duplication and keep future changes to this behavior centralized.

## Individual Comments

### Comment 1
<location path="src/utils/use-multi-file-auth-state-prisma.ts" line_range="185" />
<code_context>
               let value = await readData(`${type}-${id}`);
               if (type === 'app-state-sync-key' && value) {
-                value = proto.Message.AppStateSyncKeyData.create(value);
+                value = proto.Message.AppStateSyncKeyData.fromObject(value);
               }

</code_context>
<issue_to_address>
**suggestion:** Consider extracting the AppStateSyncKey deserialization into a shared helper to avoid duplication across providers.

This `AppStateSyncKeyData.fromObject` logic is now duplicated in the Prisma, file-based, and Redis implementations. Pulling it into a shared helper (e.g., `deserializeAppStateSyncKey(type, value)`) would keep behavior consistent if the proto or conversion changes and simplify handling future edge cases like versioned payloads across all providers.

Suggested implementation:

```typescript
              let value = await readData(`${type}-${id}`);
              value = deserializeAppStateSyncKey(type, value);

              data[id] = value;

```

To fully implement the suggestion, you should also:

1. Add an import for the shared helper at the top of this file:
   - `import { deserializeAppStateSyncKey } from './app-state-sync-key';`  
     (adjust the relative path to match your existing utils structure, e.g. `../utils/app-state-sync-key` if appropriate).

2. Create a shared helper module (e.g. `src/utils/app-state-sync-key.ts`) with behavior like:
   ```ts
   import { proto } from '@adiwajshing/baileys'; // or wherever proto comes from

   export function deserializeAppStateSyncKey(type: string, value: any) {
     if (type === 'app-state-sync-key' && value) {
       return proto.Message.AppStateSyncKeyData.fromObject(value);
     }

     return value;
   }
   ```

3. Update the file-based and Redis implementations to also use this helper instead of inlining:
   ```ts
   value = deserializeAppStateSyncKey(type, value);
   ```
   at the corresponding locations where `proto.Message.AppStateSyncKeyData.fromObject(value)` is currently used.

These changes will centralize the AppStateSyncKey deserialization logic and keep behavior consistent across all providers.
</issue_to_address>

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.

let value = await readData(`${type}-${id}`);
if (type === 'app-state-sync-key' && value) {
value = proto.Message.AppStateSyncKeyData.create(value);
value = proto.Message.AppStateSyncKeyData.fromObject(value);

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.

suggestion: Consider extracting the AppStateSyncKey deserialization into a shared helper to avoid duplication across providers.

This AppStateSyncKeyData.fromObject logic is now duplicated in the Prisma, file-based, and Redis implementations. Pulling it into a shared helper (e.g., deserializeAppStateSyncKey(type, value)) would keep behavior consistent if the proto or conversion changes and simplify handling future edge cases like versioned payloads across all providers.

Suggested implementation:

              let value = await readData(`${type}-${id}`);
              value = deserializeAppStateSyncKey(type, value);

              data[id] = value;

To fully implement the suggestion, you should also:

  1. Add an import for the shared helper at the top of this file:

    • import { deserializeAppStateSyncKey } from './app-state-sync-key';
      (adjust the relative path to match your existing utils structure, e.g. ../utils/app-state-sync-key if appropriate).
  2. Create a shared helper module (e.g. src/utils/app-state-sync-key.ts) with behavior like:

    import { proto } from '@adiwajshing/baileys'; // or wherever proto comes from
    
    export function deserializeAppStateSyncKey(type: string, value: any) {
      if (type === 'app-state-sync-key' && value) {
        return proto.Message.AppStateSyncKeyData.fromObject(value);
      }
    
      return value;
    }
  3. Update the file-based and Redis implementations to also use this helper instead of inlining:

    value = deserializeAppStateSyncKey(type, value);

    at the corresponding locations where proto.Message.AppStateSyncKeyData.fromObject(value) is currently used.

These changes will centralize the AppStateSyncKey deserialization logic and keep behavior consistent across all providers.

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