End-to-end encryption in Javascript, PHP and MySQL

16 Feb, 2023

End-to-end encryption
Javascript
PHP
MySQL
End-to-end encryption in Javascript, PHP and MySQL

End-to-end encrypted chats are more secured than the ones where encryption is done on the server side. Because the messages get encrypted even before sending them to the server. This will prevent any read or alter operation of messages in-transit. Let's learn how to do it.


We will be using Javascript for encryption and decryption. And we will be using PHP for handling AJAX requests. All the encrypted messages will be stored in MySQL database.

Table of contents

  1. Setup database
  2. Private and public keys
  3. Encrypt message
  4. Decrypt message
End-to-end encryption mechanism

Setup database

First, open your phpMyAdmin and create a database named end_to_end_encryption. Then create a file named db.php and write the following code in it.

<?php

$conn = new PDO("mysql:host=localhost;dbname=end_to_end_encryption", "root", "");

The second and third parameters are username and password to the database. You can change them as per your server. Then we will create a file named index.php and write the following code in it.

<?php

require_once "db.php";

$sql = "CREATE TABLE IF NOT EXISTS users(
  id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  privateKey TEXT DEFAULT NULL,
  publicKey TEXT DEFAULT NULL
)";
$conn->prepare($sql)->execute();

$sql = "CREATE TABLE IF NOT EXISTS messages(
  id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
  sender VARCHAR(255) NOT NULL,
  receiver VARCHAR(255) NOT NULL,
  message TEXT NOT NULL,
  iv TEXT NOT NULL
)";
$conn->prepare($sql)->execute();

?>

This will create 2 tables. One for users where we will store each user's private and public key. Second table where we will store all our encrypted messages. We will also store IV (initialization vector) required for decrypting the message. The IV will also be encrypted. Run the following URL in the browser.

http://localhost/end-to-end-encryption-js-php-mysql/index.php

You need to insert 2 users manually in the user's table to properly understand the mechanism of end-to-end encryption.

We assume that you have a folder named end-to-end-encryption-js-php-mysql where you placed your index.php file. After running the above URL in the browser, you need to check your phpMyAdmin. You will now see your 2 tables created.

Private and public keys

Private and public keys of each user is unique and it is used to encrypt and decrypt the messages. We will encrypt the message using sender's private key with receiver's public key. Similarly, we will decrypt the message using logged-in user's private and other user's public key. So we will create a form in our index.php file.

<form onsubmit="doLogin(this)">
  <input type="email" name="email" id="email" placeholder="Enter email" />
  <input type="submit" value="Login" />
</form>

You need to perform the function below in your own login module. We are not going into the authentication because that is not in the scope of this tutorial. When the form submits, we will call an AJAX request to authenticate the user.

<script>
  function doLogin() {
    event.preventDefault()
    
    const form = event.target
    const formData = new FormData(form)
    
    const ajax = new XMLHttpRequest()
    ajax.open("POST", "login.php", true)
    ajax.onreadystatechange = function () {
      if (this.readyState == 4 && this.status == 200) {
        if (!this.responseText) {
          updateKeys()
        }
      }
    }

    ajax.send(formData)
  }
</script>

Create a file named login.php that will tell if the logged-in user has private and public keys.

<?php

require_once "db.php";
$email = $_POST["email"];

$sql = "SELECT publicKey FROM users WHERE email = ?";
$result = $conn->prepare($sql);
$result->execute([
  $email
]);
$user = $result->fetchObject();

echo ($user && $user->publicKey != null);
exit();

This will return true or false indicating if the user has public key in the database. If not, then the client side will call another AJAX request from the function updateKeys() to generate the keys and save them in database.

async function updateKeys() {
      const keyPair = await window.crypto.subtle.generateKey(
          {
              name: "ECDH",
              namedCurve: "P-256",
          },
          true,
          ["deriveKey", "deriveBits"]
      )

      const publicKeyJwk = await window.crypto.subtle.exportKey(
          "jwk",
          keyPair.publicKey
      )

      const privateKeyJwk = await window.crypto.subtle.exportKey(
          "jwk",
          keyPair.privateKey
      )

      const formData = new FormData()
      formData.append("email", document.getElementById("email").value)
      formData.append("publicKey", JSON.stringify(publicKeyJwk))
      formData.append("privateKey", JSON.stringify(privateKeyJwk))

      const ajax = new XMLHttpRequest()
      ajax.open("POST", "update-keys.php", true)
      ajax.onreadystatechange = function () {
        if (this.readyState == 4 && this.status == 200) {
          console.log(this.responseText)
        }
      }

      ajax.send(formData)
}

We are using P-256 curve algorithm to generate a key pair. Then we are exporting private and public keys JWK (JSON Web Token). To save them in database, we are converting them to JSON string. Now we need to create a file named update-keys.php that will update those keys in user's table.

<?php

require_once "db.php";

$email = $_POST["email"];
$publicKey = $_POST["publicKey"];
$privateKey = $_POST["privateKey"];

$sql = "UPDATE users SET publicKey = ?, privateKey = ? WHERE email = ?";
$result = $conn->prepare($sql);
$result->execute([
  $publicKey,
  $privateKey,
  $email
]);

echo "Updated";
exit();

Try running the index.php file again. Enter any of the user's email address from database and hit "Login". You will see the message "Updated" in the browser console. But you will see it just once, because once the public keys are updated, this function won't gets called. If you check your phpMyAdmin, you will see that the private and public key of that user will be updated. You should do that for both users so each user will have its own private and public keys.

Encrypt message

Now that each user has its own private and public keys, we can use them to encrypt messages and save them in database. Create a file named send.php that will display a form to enter sender and receiver's email addresses and a message to encrypt.

<form onsubmit="sendMessage()" id="form-message">
  <input type="email" name="sender" placeholder="Sender email" />
  <input type="email" name="receiver" placeholder="Receiver email" />
  <textarea name="message" placeholder="Message"></textarea>
  <input type="submit" value="Send" />
</form>

We will create 2 Javascript variables that will hold the sender's private key and receiver's public key values.

<script>
  let publicKey = ""
  let privateKey = ""
</script>

We are using let because these values will be updated later. Create a function that will be called when the above form submits.

function sendMessage() {
  event.preventDefault()

  if (publicKey == "" || privateKey == "") {
    const form = event.target
    const formData = new FormData(form)
    
    const ajax = new XMLHttpRequest()
    ajax.open("POST", "get-keys.php", true)
    ajax.onreadystatechange = function () {
      if (this.readyState == 4 && this.status == 200) {
        const response = JSON.parse(this.responseText)
        privateKey = JSON.parse(response[0])
        publicKey = JSON.parse(response[1])
        doSendMessage()
      }
    }

    ajax.send(formData)
  } else {
    doSendMessage()
  }
}

This will first check if the private and public keys are already fetched. If fetched, then it will call doSendMessage() function that we will create later. If not fetched, then we will first fetch the keys and then call the 2nd function. We are using this check because if you are sending multiple messages to the same recipient, then it should not get private and public keys on each send message request.


Now we will create a file named get-keys.php to fetch the sender's private key and receiver's public key.

<?php

require_once "db.php";

$sender = $_POST["sender"];
$receiver = $_POST["receiver"];

$sql = "SELECT privateKey FROM users WHERE email = ?";
$result = $conn->prepare($sql);
$result->execute([
  $sender
]);
$userSender = $result->fetchObject();

$sql = "SELECT publicKey FROM users WHERE email = ?";
$result = $conn->prepare($sql);
$result->execute([
  $receiver
]);
$userReceiver = $result->fetchObject();

echo json_encode([
  $userSender->privateKey,
  $userReceiver->publicKey
]);
exit();

When the keys are returned on the client side, the variables will be updated and the second function will be called to send the message.

async function doSendMessage() {
      const form = document.getElementById("form-message")
      const formData = new FormData()
      formData.append("sender", form.sender.value)
      formData.append("receiver", form.receiver.value)

      const publicKeyObj = await window.crypto.subtle.importKey(
          "jwk",
          publicKey,
          {
              name: "ECDH",
              namedCurve: "P-256",
          },
          true,
          []
      )

      const privateKeyObj = await window.crypto.subtle.importKey(
          "jwk",
          privateKey,
          {
              name: "ECDH",
              namedCurve: "P-256",
          },
          true,
          ["deriveKey", "deriveBits"]
      )

      const derivedKey = await window.crypto.subtle.deriveKey(
          { name: "ECDH", public: publicKeyObj },
          privateKeyObj,
          { name: "AES-GCM", length: 256 },
          true,
          ["encrypt", "decrypt"]
      )

      const encodedText = new TextEncoder().encode(form.message.value)
      const iv = new TextEncoder().encode(new Date().getTime())
      const encryptedData = await window.crypto.subtle.encrypt(
          { name: "AES-GCM", iv: iv },
          derivedKey,
          encodedText
      )
      const uintArray = new Uint8Array(encryptedData)
      const string = String.fromCharCode.apply(null, uintArray)
      const base64Data = btoa(string)
      const b64encodedIv = btoa(new TextDecoder("utf8").decode(iv))

      formData.append("message", base64Data)
      formData.append("iv", b64encodedIv)
  
      const ajax = new XMLHttpRequest()
      ajax.open("POST", "send-message.php", true)
      ajax.onreadystatechange = function () {
        if (this.readyState == 4 && this.status == 200) {
          console.log(this.responseText)
        }
      }

      ajax.send(formData)
}

We will use the same P-256 curve algorithm to import the private and public keys we used to exporting it. Then we will create derived key from both (private and public) keys. We will use the derived key, IV and encoded message to encrypt the message. Once the message is encrypted, we will convert the encrypted message and IV to base64 string and send them in the AJAX request. IV will be used to decrypt the message. Then we will create a file named send-message.php to save the data in the database.

<?php

require_once "db.php";

$sender = $_POST["sender"];
$receiver = $_POST["receiver"];
$message = $_POST["message"];
$iv = $_POST["iv"];

$sql = "INSERT INTO messages(sender, receiver, message, iv) VALUES (?, ?, ?, ?)";
$result = $conn->prepare($sql);
$result->execute([
  $sender,
  $receiver,
  $message,
  $iv
]);
echo $conn->lastInsertId();

Run the file send.php in the browser. Enter sender and receiver's email address, type the message and hit "send". If all goes well, then you will see the inserted message ID in the browser console.

Decrypt message

Now we need to decrypt the encrypted messages. Create a file named read.php. Here we will create a form to enter sender and receiver's email address to fetch their messages.

<form onsubmit="readMessages()" id="form-read">
  <input type="email" name="sender" placeholder="Sender email" />
  <input type="email" name="receiver" placeholder="Receiver email" />
  <input type="submit" value="Read" />
</form>

Then we will create a Javascript function that will be called when the above form submits.

function readMessages() {
  event.preventDefault()

  const form = event.target
  const formData = new FormData(form)
  
  const ajax = new XMLHttpRequest()
  ajax.open("POST", "get-messages.php", true)
  ajax.onreadystatechange = async function () {
    if (this.readyState == 4 && this.status == 200) {
      const response = JSON.parse(this.responseText)

      const publicKeyObj = await window.crypto.subtle.importKey(
          "jwk",
          JSON.parse(response.publicKey),
          {
              name: "ECDH",
              namedCurve: "P-256",
          },
          true,
          []
      )

      const privateKeyObj = await window.crypto.subtle.importKey(
          "jwk",
          JSON.parse(response.privateKey),
          {
              name: "ECDH",
              namedCurve: "P-256",
          },
          true,
          ["deriveKey", "deriveBits"]
      )

      const derivedKey = await window.crypto.subtle.deriveKey(
          { name: "ECDH", public: publicKeyObj },
          privateKeyObj,
          { name: "AES-GCM", length: 256 },
          true,
          ["encrypt", "decrypt"]
      )

      for (let a = 0; a < response.messages.length; a++) {
        const iv = new Uint8Array(atob(response.messages[a].iv).split("").map(function(c) {
            return c.charCodeAt(0)
        }))
        const initializationVector = new Uint8Array(iv).buffer
        const string = atob(response.messages[a].message)
        const uintArray = new Uint8Array(
            [...string].map((char) => char.charCodeAt(0))
        )
        const decryptedData = await window.crypto.subtle.decrypt(
            {
                name: "AES-GCM",
                iv: initializationVector,
            },
            derivedKey,
            uintArray
        )
        const message = new TextDecoder().decode(decryptedData)
        console.log(message)
      }
    }
  }

  ajax.send(formData)
}

This will call an AJAX request to get the messages. The API will also return the private and public keys required to decrypt the message. Same code can be used to import the keys that we used for sending the message. Create a file named get-messages.php and write the following code in it.

<?php

require_once "db.php";

$sender = $_POST["sender"];
$receiver = $_POST["receiver"];

$sql = "SELECT * FROM messages WHERE sender = ? AND receiver = ?";
$result = $conn->prepare($sql);
$result->execute([
  $sender,
  $receiver
]);
$messages = $result->fetchAll(PDO::FETCH_OBJ);

$sql = "SELECT privateKey FROM users WHERE email = ?";
$result = $conn->prepare($sql);
$result->execute([
  $sender
]);
$userSender = $result->fetchObject();

$sql = "SELECT publicKey FROM users WHERE email = ?";
$result = $conn->prepare($sql);
$result->execute([
  $receiver
]);
$userReceiver = $result->fetchObject();

echo json_encode([
  "messages" => $messages,
  "privateKey" => $userSender->privateKey,
  "publicKey" => $userReceiver->publicKey
]);
exit();

If you run the read.php file now, you will see the decrypted messages in console tab. However, if you see the "network" tab of browser, you will see that the messages are being returned encrypted from the server. That means that your messages are decrypted online when they arrived on the client side. Thus, they are safe in-transit.

That's how you can do end-to-end encryption in Javascript with PHP and MySQL. No external library has been used in this tutorial, so the code used here will work on all frameworks.

About Author

Adnan Afzal

Developer, technology evangalist, loves to read and write about new technologies. Madly in love with backend development. Follow him on social media.