javascript
php
encryption
decryption
shared-secret-key

Simple Javascript encrypt, PHP decrypt with shared secret key

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

If JavaScript encrypts data and PHP decrypts it, both sides must agree on more than a shared secret. They must also use the same cipher, key derivation method, mode of operation, initialization vector handling, and output encoding.

Pick a Common Format First

The easiest way to make browser and PHP crypto interoperate is to define a transport format explicitly. A practical format is:

  • derive a 256-bit key from a passphrase with SHA-256
  • generate a random 16-byte IV
  • encrypt with AES-256-CBC
  • prepend the IV to the ciphertext
  • Base64-encode the result

The browser can do this with the Web Crypto API. PHP can reverse the process with openssl_decrypt.

JavaScript Encryption Example

This example takes a passphrase and plaintext, derives the AES key, and returns a Base64 string that includes the IV plus ciphertext.

javascript
1async function encryptMessage(plaintext, passphrase) {
2  const encoder = new TextEncoder();
3  const passphraseBytes = encoder.encode(passphrase);
4  const hash = await crypto.subtle.digest("SHA-256", passphraseBytes);
5
6  const key = await crypto.subtle.importKey(
7    "raw",
8    hash,
9    { name: "AES-CBC" },
10    false,
11    ["encrypt"]
12  );
13
14  const iv = crypto.getRandomValues(new Uint8Array(16));
15  const encrypted = await crypto.subtle.encrypt(
16    { name: "AES-CBC", iv },
17    key,
18    encoder.encode(plaintext)
19  );
20
21  const combined = new Uint8Array(iv.length + encrypted.byteLength);
22  combined.set(iv, 0);
23  combined.set(new Uint8Array(encrypted), iv.length);
24
25  return btoa(String.fromCharCode(...combined));
26}
27
28encryptMessage("server-shared payload", "correct horse battery staple")
29  .then(console.log);

The important part is that the IV is random for every message. Reusing a fixed IV with CBC mode weakens security badly.

PHP Decryption Example

On the PHP side, decode the Base64 string, split out the first 16 bytes as the IV, then decrypt the rest.

php
1<?php
2function decryptMessage(string $payload, string $passphrase): string {
3    $raw = base64_decode($payload, true);
4    if ($raw === false || strlen($raw) < 17) {
5        throw new InvalidArgumentException('Invalid payload');
6    }
7
8    $iv = substr($raw, 0, 16);
9    $ciphertext = substr($raw, 16);
10    $key = hash('sha256', $passphrase, true);
11
12    $plaintext = openssl_decrypt(
13        $ciphertext,
14        'aes-256-cbc',
15        $key,
16        OPENSSL_RAW_DATA,
17        $iv
18    );
19
20    if ($plaintext === false) {
21        throw new RuntimeException('Decryption failed');
22    }
23
24    return $plaintext;
25}
26
27$input = 'PASTE_BASE64_HERE';
28echo decryptMessage($input, 'correct horse battery staple'), PHP_EOL;

If the JavaScript and PHP examples use the same passphrase, the same algorithm, and the same encoding rules, they interoperate cleanly.

Why Shared-Secret Encryption Is Tricky in Browsers

The main risk is not the AES call itself. The hard part is key management. If the browser already has the shared secret, anyone who can inspect the running client can eventually recover it. That means client-side symmetric encryption is usually useful for compatibility or defense in depth, not as a substitute for server-side trust boundaries.

You should also prefer authenticated encryption for new systems. AES-CBC only encrypts. It does not prove the ciphertext was not modified. In modern designs, AES-GCM is usually the better choice because it combines confidentiality and integrity.

Safer Variant With Authentication

If you can control both sides fully, use an authenticated scheme. In PHP that often means moving to an AEAD-capable API or using a higher-level library. The browser side can use AES-GCM through Web Crypto, but then the server code must match the nonce and tag format exactly.

If you must stay with CBC for legacy reasons, add a separate message authentication code over the IV and ciphertext before decrypting.

Common Pitfalls

The most common failure is inconsistent key derivation. If JavaScript hashes the passphrase once but PHP pads or truncates the raw string directly, the keys differ and decryption fails.

Another frequent mistake is forgetting to send the IV. CBC mode needs the same IV for decryption, so you must include it with the payload. The IV is not secret, but it must be unique per message.

Encoding mismatches also break interoperability. One side may output Base64 while the other expects hex, or one side may treat the ciphertext as UTF-8 text. Treat encrypted bytes as bytes until you deliberately encode them for transport.

Finally, do not rely on this pattern as the only protection for browser-to-server traffic. You still need HTTPS. Transport security protects the whole session, including metadata and credentials that your custom encryption layer does not cover.

Summary

  • JavaScript and PHP must share the same cipher, key derivation, IV format, and encoding.
  • A simple interoperable pattern is AES-256-CBC with SHA-256 key derivation and Base64 transport.
  • Send the IV with the ciphertext and generate a new IV for every message.
  • Prefer authenticated encryption for new designs.
  • Keep HTTPS in place even when you add application-level encryption.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.