# How E2EE Encryption Works in LINEJS Using Curve25519 and TweetNaCl

> Discover how LINEJS achieves E2EE encryption using curve25519 and tweetnacl. Explore X25519 key exchange and AES-256-GCM key derivation for secure messaging.

- Repository: [Evex  Developers/linejs](https://github.com/evex-dev/linejs)
- Tags: deep-dive
- Published: 2026-03-01

---

**LINEJS implements end-to-end encryption by combining X25519 Diffie-Hellman key exchange via `curve25519-js` with Curve25519 key-pair generation via `tweetnacl`, deriving AES-256-GCM keys through SHA-256 stretching to secure message payloads.**

The `evex-dev/linejs` repository provides a full-stack TypeScript client for the LINE messaging protocol, including a complete E2EE implementation that interoperates with the official LINE mobile apps. The cryptography stack relies on two lightweight, audited libraries to handle the elliptic-curve operations and symmetric encryption, orchestrated by the `E2EE` class in [`packages/linejs/base/e2ee/mod.ts`](https://github.com/evex-dev/linejs/blob/main/packages/linejs/base/e2ee/mod.ts).

## Cryptographic Primitives: Curve25519-JS and TweetNaCl

LINEJS delegates low-level cryptographic operations to two distinct libraries, each serving a specific purpose in the E2EE lifecycle:

| Library | Purpose | Key Function |
|---------|---------|--------------|
| **`curve25519-js`** | X25519 Diffie-Hellman scalar multiplication | `sharedKey(privateKey, publicKey)` |
| **`tweetnacl`** | Curve25519 key-pair generation | `nacl.box.keyPair()` |

The `curve25519-js` library performs the X25519 ECDH operation that transforms a local private key and a remote public key into a shared secret. TweetNaCl is used primarily for generating the initial identity key pairs during client initialization and for the temporary key pairs required during QR-code login flows.

## Key Management and Storage

The E2EE system maintains persistent key material using the client's storage interface, prefixed with `e2eeKeys:`.

### Self Key Retrieval

Before encrypting or decrypting, the client loads its own key pair via `getE2EESelfKeyData` in [`packages/linejs/base/e2ee/mod.ts`](https://github.com/evex-dev/linejs/blob/main/packages/linejs/base/e2ee/mod.ts):

```typescript
// Load own key pair or fetch the newest one from the server
const selfKeyData = await this.getE2EESelfKeyData(_from);

```

If no local key exists, the client negotiates a new key pair with the LINE servers and stores it under the `e2eeKeys:` prefix.

### Peer Key Discovery

For **one-to-one chats**, the client calls `talk.negotiateE2EEPublicKey` to fetch the recipient's public key from LINE's key distribution servers. For **group chats**, the client uses the pre-fetched group public key stored under the `e2eePublicKeys:` prefix.

## The E2EE Encryption Flow

The encryption process follows a standard Encrypt-then-MAC pattern using AES-256-GCM, with keys derived from an X25519 shared secret.

### 1. Shared Secret Derivation

The client generates the shared secret using `generateSharedSecret`, which wraps `curve25519-js`:

```typescript
const keyData = this.generateSharedSecret(privateKey, remotePublicKey);

```

Internally, this calls `sharedKey(privateKey, publicKey)` from `curve25519-js` to perform the X25519 scalar multiplication.

### 2. Key Stretching

For legacy E2EE v1, the shared secret is stretched using SHA-256 with domain separation constants:

```typescript
const aesKey = this.getSHA256Sum(Buffer.from(sharedSecret), "Key");
const aesIv  = this.xor(this.getSHA256Sum(Buffer.from(sharedSecret), "IV"));

```

The `"Key"` and `"IV"` strings act as domain separators to produce distinct key material for encryption and initialization vectors.

### 3. AES-256-GCM Encryption (v2)

Modern LINE clients use E2EE v2, implemented in `encryptE2EEMessageV2` (lines 637-649 in [`mod.ts`](https://github.com/evex-dev/linejs/blob/main/mod.ts)):

1. **Generate random salt** (16 bytes) and **sign** (12 bytes, used as GCM nonce).
2. **Derive GCM key**: `SHA256(keyData || salt || "Key")`.
3. **Construct AAD** (Additional Authenticated Data) encoding `to`, `from`, sender key ID, receiver key ID, protocol version, and content type.
4. **Encrypt payload** using `crypto.createCipheriv("aes-256-gcm", gcmKey, sign)`.
5. **Package chunks**: `[salt, ciphertext+authTag, sign, senderKeyId, receiverKeyId]`.

The `encryptE2EETextMessage` helper (lines 438-459) wraps this flow for text content, automatically serializing the text into a JSON payload.

## Decrypting E2EE Messages

Decryption mirrors the encryption process. When a message arrives with the `e2ee` flag, the client invokes `decryptE2EEMessage` (lines 511-525), which dispatches to `decryptE2EEMessageV2` (lines 929-949).

The decryption steps:

1. **Load keys**: Retrieve the local private key and the sender's public key using `getE2EELocalPublicKey`.
2. **Re-derive shared secret**: Call `generateSharedSecret` with the local private key and sender's public key to produce the identical `keyData` used during encryption.
3. **Reconstruct GCM key**: Calculate `SHA256(keyData || salt || "Key")` using the salt from the first chunk.
4. **Verify and decrypt**: Use `crypto.createDecipheriv("aes-256-gcm", gcmKey, sign)` with the sign (nonce) from the third chunk, verifying the authentication tag appended to the ciphertext.
5. **Parse payload**: Extract the JSON content containing the text or location data.

If the authentication tag verification fails, the decryption throws an error, preventing tampered or corrupted messages from being processed.

## Integration with the Talk API

The E2EE system is transparently integrated into the high-level messaging API. In [`packages/linejs/base/service/talk/mod.ts`](https://github.com/evex-dev/linejs/blob/main/packages/linejs/base/service/talk/mod.ts), the `sendMessage` method (lines 4-26) checks for the `e2ee` flag:

```typescript
if (e2ee && !chunks) {
  const encryptedChunks = await this.client.e2ee.encryptE2EEMessage(
    to,
    text,
    contentType
  );
  // ... attach encryptedChunks to the message payload
}

```

When `e2ee: true` is passed without pre-encrypted chunks, the client automatically:
1. Resolves the recipient's public key
2. Generates the shared secret
3. Encrypts the payload
4. Packages the chunks for transmission

This allows developers to send encrypted messages with a single flag while maintaining full compatibility with the LINE protocol.

## QR Login Secret Generation

For QR-code based authentication, LINEJS uses TweetNaCl to generate ephemeral key pairs. The `createSqrSecret` method (lines 1020-1036 in [`mod.ts`](https://github.com/evex-dev/linejs/blob/main/mod.ts)) implements this:

```typescript
const { secretKey, publicKey } = nacl.box.keyPair();

```

The method returns:
- `secretKey`: A Uint8Array kept client-side to decrypt the QR login response
- `sqrSecret`: A URL-encoded string representation of the public key appended to the QR code URL

This temporary key pair is used only during the authentication handshake and is discarded afterward, ensuring that long-term identity keys are not exposed during the QR scanning process.

## Summary

LINEJS implements a robust end-to-end encryption system that interoperates with the official LINE mobile applications:

- **Curve25519-JS** handles the X25519 Diffie-Hellman scalar multiplication to derive shared secrets between peers.
- **TweetNaCl** provides secure Curve25519 key-pair generation for identity keys and QR-login ephemeral secrets.
- The **E2EE class** in [`packages/linejs/base/e2ee/mod.ts`](https://github.com/evex-dev/linejs/blob/main/packages/linejs/base/e2ee/mod.ts) orchestrates key management, shared secret derivation via SHA-256 stretching, and AES-256-GCM encryption/decryption.
- **E2EE v2** uses a chunks array format containing salt, ciphertext with authentication tag, nonce, and key identifiers.
- The **Talk API** seamlessly integrates encryption via the `e2ee: true` flag, automatically handling key negotiation and payload encryption.

## Frequently Asked Questions

### What is the difference between curve25519-js and tweetnacl in LINEJS?

**Curve25519-js** is used exclusively for the X25519 Diffie-Hellman operation (`sharedKey`) to compute a shared secret from a private key and a remote public key. **TweetNaCl** is used for generating Curve25519 key pairs (`nacl.box.keyPair`) during client initialization and for creating temporary key pairs during QR-code login flows. While both libraries use Curve25519, curve25519-js focuses on scalar multiplication, whereas TweetNaCl provides higher-level box primitives and secure random key generation.

### How does LINEJS derive the AES key for E2EE messages?

LINEJS derives the AES key through a multi-step process defined in [`packages/linejs/base/e2ee/mod.ts`](https://github.com/evex-dev/linejs/blob/main/packages/linejs/base/e2ee/mod.ts). First, it generates a shared secret using `generateSharedSecret`, which calls `sharedKey` from curve25519-js to perform X25519 Diffie-Hellman. For legacy v1, it stretches this secret using `getSHA256Sum(sharedSecret, "Key")` for the AES key and an XOR of `getSHA256Sum(sharedSecret, "IV")` for the initialization vector. For modern v2 encryption, it computes `SHA256(keyData || salt || "Key")` to produce the 256-bit AES-GCM key.

### What is the 'chunks' array structure in LINEJS E2EE messages?

The `chunks` array is the binary payload format used to transmit encrypted messages in LINEJS, constructed in `encryptE2EEMessageV2`. The array contains exactly five elements in order: 
1. **Salt** (16 bytes) - random entropy used in key derivation; 
2. **Ciphertext + Auth Tag** - the AES-256-GCM encrypted payload concatenated with its 16-byte authentication tag; 
3. **Sign** (12 bytes) - the nonce (IV) used for the GCM cipher; 
4. **Sender Key ID** - identifier for the sender's public key; 
5. **Receiver Key ID** - identifier for the recipient's public key. 
This structure allows the recipient to reconstruct the exact parameters needed for decryption and authentication verification.

### Is LINEJS E2EE compatible with the official LINE mobile app?

Yes, the LINEJS E2EE implementation is fully compatible with the official LINE mobile applications because it follows the same protocol specifications used by LINE Corporation's official clients. The implementation uses standard X25519 for key exchange and AES-256-GCM for message encryption, matching the cryptographic primitives used in the mobile apps. The `chunks` array format, key identifiers, and AAD (Additional Authenticated Data) structure conform to the LINE E2EE v2 specification, ensuring that messages encrypted by LINEJS can be decrypted by official clients and vice versa.