How to Implement Message Handlers for Encrypted Messages in LINEJS

Implementing message handlers for encrypted messages in LINEJS requires using the E2EE class to encrypt payloads with encryptE2EEMessage and decrypt them with decryptE2EEMessage, while TalkService.sendMessage automatically handles the encryption flow when the e2ee flag is enabled.

The evex-dev/linejs repository provides native end-to-end encryption (E2EE) support for LINE Talk messages through a dedicated cryptographic module in the base package. This guide demonstrates how to implement secure message handlers that encrypt outgoing traffic and decrypt incoming payloads using the actual source implementation.

Understanding E2EE Architecture in LINEJS

The LINEJS encryption flow follows a five-step lifecycle: detect the encryption requirement, encrypt the payload via E2EE.encryptE2EEMessage, transmit through TalkService, receive the encrypted response, and decrypt using E2EE.decryptE2EEMessage. All cryptographic operations reside in packages/linejs/base/e2ee/mod.ts, while the high-level messaging API in packages/linejs/base/service/talk/mod.ts orchestrates the automatic encryption paths.

Sending Encrypted Messages

The entry point for sending encrypted messages is TalkService.sendMessage in packages/linejs/base/service/talk/mod.ts (lines 65‑78). When e2ee is set to true and no raw chunks are supplied, the method automatically delegates encryption to the E2EE module before transmission:

if ((e2ee && !chunks && location) || (e2ee && !chunks && text)) {
    const chunks = await this.client.e2ee.encryptE2EEMessage(
        to,
        text || location || "invalid",
        contentType,
    );
    // …build contentMetadata with e2eeVersion/e2eeMark…
    return this.sendMessage({ …options, chunks });
}

The E2EE.encryptE2EEMessage method in packages/linejs/base/e2ee/mod.ts (lines 438‑447) handles the cryptographic workflow:

public async encryptE2EEMessage(
    to: string,
    data: string | Location | Record<string, LooseType>,
    contentType: LINETypes.ContentType = 0,
    specVersion = 2,
): Promise<Buffer[]> {
    // 1️⃣ Resolve sender/receiver keys
    // 2️⃣ Generate a shared secret (Diffie‑Hellman) → `keyData`
    // 3️⃣ Choose the proper branch (text, location, generic data)
    // 4️⃣ Build AAD, GCM key, sign, and encrypt (V2)
}

This method resolves the sender's private key from local storage, fetches the receiver's public key via talk.negotiateE2EEPublicKey for one-to-one chats or cached group key data for groups, generates a Curve25519 shared secret, and produces an array of five buffers: salt, ciphertext+tag, sign, senderKeyId, and receiverKeyId.

Receiving and Decrypting Messages

When handling incoming messages, the E2EE.decryptE2EEMessage method in packages/linejs/base/e2ee/mod.ts (lines 651‑674) serves as the primary handler:

public async decryptE2EEMessage(messageObj: Message): Promise<Message> {
    if ((messageObj.contentType === "NONE" || …) && messageObj.chunks) {
        const [text, meta] = await this.decryptE2EETextMessage(messageObj);
        messageObj.text = text;
        messageObj.contentMetadata = { …messageObj.contentMetadata, …meta };
    } else if (messageObj.contentType === "LOCATION" && messageObj.chunks) {
        messageObj.location = await this.decryptE2EELocationMessage(messageObj);
    }
    return messageObj;
}

For text messages, decryptE2EETextMessage extracts the five buffers and calls decryptE2EEMessageV2 (lines 928‑966), which re-derives the GCM key from the shared secret and salt, then performs AES-256-GCM decryption using the sign buffer as the IV.

Automatic Fallback Mechanism

The TalkService.sendMessage implementation includes automatic E2EE detection. If the server returns an error containing "E2EE" and the original request did not set the encryption flag, the method catches the error, toggles options.e2ee = true, and retries automatically (lines 62‑70).

Implementing the Message Handler Flow

Sending Encrypted Text Messages

To implement a handler that sends encrypted messages, invoke sendMessage with the e2ee flag enabled:

import { LineClient } from "@evex/linejs";

// Assume `client` is an authenticated LineClient
await client.talk.sendMessage({
  to: "U1234567890abcdef",   // recipient MID
  text: "Secret hello 👀",    // plain text
  e2ee: true,                // enable E2EE (optional – auto‑detect works)
});

This triggers the encryption branch in TalkService, generating encrypted chunks and setting metadata fields e2eeVersion, e2eeMark, and numeric key IDs automatically.

Handling Incoming Encrypted Messages

Implement message handlers that process incoming encrypted data by checking for chunks and invoking the decryption method:

// Pull latest events (including messages)
const events = await client.talk.sync({ limit: 20 });

// Find a regular message event
for (const ev of events) {
  if (ev.type === "RECEIVE_MESSAGE") {
    const msg = ev.message as Message;
    // Decrypt if needed (the method is a no‑op for plain messages)
    const decrypted = await client.e2ee.decryptE2EEMessage(msg);
    console.log("From:", decrypted.from, "Text:", decrypted.text);
  }
}

The decryptE2EEMessage method inspects contentType and chunks, performing the appropriate decryption path (text, location, or data) and populating the decrypted fields on the message object.

Manual Encryption for Storage

For use cases requiring message persistence before sending, encrypt manually using the E2EE class:

import { Buffer } from "node:buffer";

const encryptedChunks = await client.e2ee.encryptE2EEMessage(
  "U1234567890abcdef",
  "Stored secret",
);
// `encryptedChunks` is an array of Buffers you can persist.

To decrypt stored chunks later, reconstruct a message object and process it:

import type { Message } from "@evex/linejs-types";

const storedMsg: Message = {
  from: client.profile!.mid,
  to: "U1234567890abcdef",
  contentType: "NONE",
  chunks: encryptedChunks,
  // …other required fields…
};

const plain = await client.e2ee.decryptE2EEMessage(storedMsg);
console.log(plain.text); // → "Stored secret"

Core Cryptographic Implementation

Key Exchange and Shared Secrets

The LINEJS E2EE implementation uses Curve25519 for Diffie-Hellman key exchange. When encrypting, the library reads the sender's private key from local storage and fetches the receiver's public key via talk.negotiateE2EEPublicKey for one-to-one chats, or uses cached group key data for group conversations. The shared secret derivation occurs in encryptE2EEMessage using the sharedKey calculation.

AEAD Encryption Details

All encrypted messages use AES-256-GCM authenticated encryption. The encryptE2EEMessageV2 method builds Additional Authenticated Data (AAD) incorporating the spec version, content type, and key IDs. The decryption process in decryptE2EEMessageV2 validates this AAD and uses the sign buffer as the IV for GCM decryption. Cryptographic primitives are provided by Node.js crypto and the tweetnacl library.

Summary

  • Automatic encryption occurs in TalkService.sendMessage when setting e2ee: true or when the server mandates encryption.
  • Core encryption logic lives in packages/linejs/base/e2ee/mod.ts, specifically the encryptE2EEMessage method which produces five buffer chunks.
  • Decryption handling uses E2EE.decryptE2EEMessage to automatically route text, location, and data messages to the appropriate decryption helpers.
  • Cryptographic foundation combines Curve25519 key exchange with AES-256-GCM authenticated encryption.
  • Storage scenarios support manual encryption and decryption for persisting encrypted messages before transmission or after reception.

Frequently Asked Questions

How does LINEJS handle E2EE key negotiation automatically?

The E2EE class fetches the receiver's public key via talk.negotiateE2EEPublicKey when encrypting messages to individual users, and retrieves cached group keys for group chats. The sender's private key is read from local storage, and the shared secret is computed using Curve25519 Diffie-Hellman without requiring manual key management in the calling code.

What content types support encryption in LINEJS?

The implementation supports text messages (contentType "NONE"), location messages (contentType "LOCATION"), and generic data messages. The decryptE2EEMessage method in packages/linejs/base/e2ee/mod.ts routes each type to specialized handlers: decryptE2EETextMessage, decryptE2EELocationMessage, or decryptE2EEDataMessage.

Can I decrypt messages stored offline with LINEJS?

Yes. Use client.e2ee.encryptE2EEMessage to generate encrypted chunks for storage, then reconstruct a message object with those chunks and call client.e2ee.decryptE2EEMessage. The method works identically for network-received and locally-stored messages as long as the chunk array and metadata are preserved.

Does LINEJS fall back to encryption if the server requires it?

Yes. According to the source in packages/linejs/base/service/talk/mod.ts, if sendMessage receives a server error containing "E2EE" and the original request did not specify encryption, the method automatically sets options.e2ee = true and retries the request without throwing the error to the caller.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →