Skip to content

bug: getData() fails for media in 1:1 E2EE chats “Unsupported state or unable to authenticate data” #88

@WhodhayX

Description

@WhodhayX

getData() fails for media in 1:1 E2EE chats — “Unsupported state or unable to authenticate data”

Title: getData() fails for media in 1:1 E2EE chats — “Unsupported state or unable to authenticate data”

Summary

message.getData() works for media from Official Accounts / non‑E2EE chats, but consistently fails for 1:1 E2EE chats with:

Unsupported state or unable to authenticate data

Text messages decrypt fine. The failure only occurs when fetching media bytes from 1:1 E2EE threads.

Environment

  • @evex/linejs: latest (repro’d with LINE_VERSION=18.6.2)
  • Node: 18.x (also reproduced on 20.x)
  • OS: macOS (Apple Silicon)
  • Auth: loginWithAuthToken (Desktop profile)
  • Storage: FileStorage (temp JSON file)

Expected vs Actual

  • Expected: message.getData() returns decrypted bytes for images in 1:1 E2EE chats (same as OA / non‑E2EE).
  • Actual: Throws Unsupported state or unable to authenticate data in 1:1 E2EE chats. OA / non‑E2EE media works.

Minimal Repro (trimmed)

import { loginWithAuthToken } from "@evex/linejs";
import { FileStorage } from "@evex/linejs/storage";

const token = process.env.LINE_AUTH_TOKEN!; // not included here
const storage = new FileStorage("/tmp/linejs-dev.json");

const client = await loginWithAuthToken(token, { device: "DESKTOPWIN", storage });
const talk = (client as any).base?.talk ?? client;

const ensureMediaPrereqs = async (chatMid?: string) => {
  // try to ensure keys/tokens flow
  await (client as any)?.base?.e2ee?.syncE2EEKey?.().catch(() => {});
  await (client as any)?.base?.talk?.e2ee?.syncE2EEKey?.().catch(() => {});
  await (client as any)?.base?.talk?.acquireEncryptedAccessToken?.({ featureType: "OBS_GENERAL" }).catch(() => {});
  if (chatMid && typeof (client as any)?.base?.talk?.determineMediaMessageFlow === "function") {
    await (client as any).base.talk.determineMediaMessageFlow({ request: { chatMid } }).catch(() => {});
  }
};

const resolveDecryptor = (c: any) => {
  const fns = [
    c?.e2ee?.decryptE2EEMessage?.bind(c?.e2ee),
    c?.base?.e2ee?.decryptE2EEMessage?.bind(c?.base?.e2ee),
    c?.base?.talk?.e2ee?.decryptE2EEMessage?.bind(c?.base?.talk?.e2ee),
    typeof c?.base?.decryptE2EEMessage === "function" ? c.base.decryptE2EEMessage.bind(c.base) : null,
    typeof c?.decryptE2EEMessage === "function" ? c.decryptE2EEMessage.bind(c) : null,
  ].filter(Boolean);
  if (!fns.length) return null;
  return async (m: any) => { for (const f of fns) { try { const out = await (f as any)(m); if (out) return out; } catch {} } return m; };
};

(client as any).on?.("message", async (m: any) => {
  const chatMid = m?.chat?.chatMid ?? m?.from ?? null;
  const rawCT = String(m?.contentType || "");
  const isImage = /IMAGE/i.test(rawCT);

  // try to decrypt the message envelope first
  const dec = await (resolveDecryptor(client) || (async (x:any)=>x))(m);

  if (isImage && typeof (dec as any)?.getData === "function") {
    try {
      // pre-warm keys/tokens/flow
      await ensureMediaPrereqs(chatMid);
      const blobOrBuf = await (dec as any).getData(); // <-- fails only in 1:1 E2EE chats
      // normalize to Buffer if Blob
      const buf = Buffer.isBuffer(blobOrBuf)
        ? blobOrBuf
        : Buffer.from(new Uint8Array(await blobOrBuf.arrayBuffer?.() || new ArrayBuffer(0)));
      console.log("image bytes", buf.byteLength);
    } catch (e:any) {
      console.warn("getData failed", e?.message || e);
    }
  }
});

await client.listen({ talk: true });

Reproduction Steps

  1. Log in with loginWithAuthToken (Desktop profile).
  2. Start listening: client.listen({ talk: true }).
  3. From a 1:1 E2EE chat, send an image to the logged-in account.
  4. In the message handler, call dec.getData() after decrypting the envelope.
  5. Observe error below. Repeat the same from an OA / non‑E2EE chat and it succeeds.

Logs (sanitized)

Successful OA / non‑E2EE image:

attachment getData starting  { rawCT: 'IMAGE', hasChunks: false, provider: 'decrypted(dec)' }
attachment getData success (first) { byteLength: 185488 }
attachment persist -> saved { kind: 'image', mime: 'image/jpeg' }

1:1 E2EE image (fails):

attachment getData starting  { rawCT: 'IMAGE', hasChunks: true, provider: 'decrypted(dec)', chatMid: 'u****************' }
attachment getData failed (first attempt) { err: 'Unsupported state or unable to authenticate data', contentType: 'IMAGE', hasChunks: true }
media prereq priming -> start
media prereq priming -> done
attachment getData failed (retry) { err: 'Unsupported state or unable to authenticate data' }

What I’ve Tried

  • Decrypting the message envelope first (decryptE2EEMessage) and then calling dec.getData().
  • Syncing E2EE keys on both base.e2ee and base.talk.e2ee.
  • Acquiring OBS encrypted access tokens via base.talk.acquireEncryptedAccessToken({ featureType: "OBS_GENERAL" }) (also tried base.obs and base.call).
  • Priming flow with base.talk.determineMediaMessageFlow({ request: { chatMid } }).
  • Retrying getData() after the above steps.
  • Using the raw message object’s getData() vs the decrypted one’s.

None of the above change the failure for 1:1 E2EE media. Non‑E2EE media continues to work.

Hypotheses

  • The E2EE media path for 1:1 chats may require an additional step (e.g., per‑message media key unwrap / MAC validation) that getData() isn’t currently performing.
  • The getData() implementation may be using the wrong E2EE context (session/device) for 1:1 media, even though envelope decryption succeeds.
  • The OBS token obtained via acquireEncryptedAccessToken(OBS_GENERAL) may be insufficient for 1:1 E2EE media blobs, or needs tying to a per‑chat flow.

What Would Help

  • Confirmation of the intended way to fetch & decrypt media bytes for 1:1 E2EE messages using @evex/linejs.
  • Guidance on any additional API calls needed (e.g., dedicated media‑key API, different featureType, or explicit e2ee.decryptMedia(...) helper).
  • If this is a bug, a fix to have message.getData() do the right thing for 1:1 E2EE media (same DX as OA / groups).

Notes

  • The same code path successfully decrypts message bodies (text) and downloads OA images.
  • Identifiers here are redacted; I can share detailed traces privately with maintainers if needed.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions