DEV Community

Cover image for How to build an end-to-end encrypted chat app in Next.js: Messages and encryption
Amarachi Iheanacho for Hackmamba

Posted on

How to build an end-to-end encrypted chat app in Next.js: Messages and encryption

Encryption ensures data security through scrambling so that only authorized people can access and decode the information or data.

What we will be building

This post discusses using the Appwrite database feature to build a chat app. In this article, we’ll ensure data security using the cryptr library for encrypting and decrypting strings.

GitHub URL

https://github.com/Iheanacho-ai/chat-app-nextjs

Prerequisites

This article is part 2 of the two-part series that describes how to build an end-to-end encrypted chat using Appwrite in a Next.js app. It is crucial to start with the first part to get the most out of this article. Check out the first part of the article here.

Installing cryptr

crypt is a simple aes-256-gcm module for encrypting and decrypting values of UTF-8 strings.

To install crypt, run this terminal command in our project directory.

    npm install cryptr
Enter fullscreen mode Exit fullscreen mode

Creating collection and attributes

On the left side of the Appwrite Console dashboard, click on the Database tab.

Appwrite Console Dashboard

Click on the Add Database button to create a new database. Creating a new database will lead us to the Collection page.

Next, we’ll create a collection in our database tab by clicking the Add Collection button. This action redirects us to a Permissions page.

Appwrite Console

At the Collection Level, we want to assign a Read Access and Write Access with a role:all value. We can customize these roles later to specify who has access to read or write to our database.

Appwrite Console

On the right side of the Permissions page, copy the Collection ID, which we’ll need to perform operations on the collection’s documents.

Next, go to the attributes tab to create the properties we want a document to have.

Appwrite Console

Let’s create a string attribute of message with a size of 256 bits.

Appwrite Console

Appwrite Console

Adding Chat app Interaction with Database

In the chat.jsx file inside pages folder, import the useState hook for handling states, Appwrite’s client instance, and Appwrite’s Databases method.

    import {  useState } from 'react';
    import { client} from '../init' 
    import { Databases } from 'appwrite';
Enter fullscreen mode Exit fullscreen mode

Next, create two state variables:

  • A messages state variable to hold the messages that a user is about to send.
  • A databaseMessages state variable to hold the messages retrieved from the database.
    const [message, setMessages] = useState("");
    const [databaseMessages, setDatabaseMessages] = useState(["hey"])
Enter fullscreen mode Exit fullscreen mode

We then create a databases instance using the Appwrite Databases method. This Databases method receives the client and the databaseID as parameters.

    const databases = new Databases(client, 'DatabaseID');
Enter fullscreen mode Exit fullscreen mode

Next, we create a listMessages function and a sendMessage function.

    const listMessages = async () => {
      const promise = await databases.listDocuments('CollectionID');
      promise.documents.map((document) => setDatabaseMessages(prevArray => [...prevArray, document.message]))
    }
Enter fullscreen mode Exit fullscreen mode

The listMessages in the code block above does the following:

  • Lists all the documents in a collection using Appwrite’s listDocuments method. This listDocuments method receives a collectionID as a parameter.
  • Updates the databaseMessages state variable with the messages saved in the documents.
    const sendMessage = async () => {
      try {
        await databases.createDocument('CollectionID', 'unique()', {
          "message": message
        });
        alert('message sent')
        setMessages("")
        listMessages()
       }catch (error) {
        console.log(error)
      }
    }
Enter fullscreen mode Exit fullscreen mode

The sendMessage function above does the following:

  • Creates a new document using Appwrite’s createDocument() function. This createDocument() function receives a collection ID, a unique() string, and attribute values as parameters.
  • Alerts us when we have successfully saved the message.
  • Clears out the message variable and calls the listMessages() function.
  • Logs any errors encountered to the console.

Next, we check whether the databaseMessages array is empty, then we loop through the data in the databaseMessages array and render the messages in our chat app.

    <div className='messages'>
       {
        databaseMessages ? (
          <div className="message-container">
             {
               databaseMessages.map((databaseMessage)=> (
                <div className="user-message">{databaseMessage}</div>
              ))
            } 
          </div>
        ) : null
      } 
    </div>
Enter fullscreen mode Exit fullscreen mode

Next, we’ll pass the message variable as a value to our input field and our sendMessage function to the onClick event listener on our send button.

    <div className='input-area'>
      <input type="text" className='message-input' value={message} onChange={(e) => setMessages(e.target.value)}/>
      <button className='send' type='button' onClick={sendMessage}>send</button>
    </div>
Enter fullscreen mode Exit fullscreen mode

After we are done with this section, this is how our chat.jsx file looks.

    import {  useState } from 'react';
    import { client} from '../init' 
    import { Databases } from 'appwrite';

    const Chat = () => {
      const [message, setMessages] = useState("");
      const [databaseMessages, setDatabaseMessages] = useState(["hey"])

      const databases = new Databases(client, 'DatabaseID');

      const listMessages = async () => {
        const promise = await databases.listDocuments('CollectionID');
        promise.documents.map((document) => setDatabaseMessages(prevArray => [...prevArray, document.message]))
      }

      const sendMessage = async () => {
        try {
          await databases.createDocument('CollectionID', 'unique()', {
            "message": message
          });
          alert('message sent')
          setMessages("")
          listMessages()
        } catch (error) {
          console.log(error)
        }
      }

      return (
        <div className='chat'>
          <div className='user-chat'>
            <div className="user-chat-header">USER</div>
            <div className='messages'>
              {
                databaseMessages.map((databaseMessage)=> (
                  <div className="user-message">{databaseMessage}</div>
                ))
              }
            </div>
            <div className='input-area'>
              <input type="text" className='message-input' value={message} onChange={(e) => setMessages(e.target.value)}/>
              <button className='send' type='button' onClick={sendMessage}>send</button>
            </div>
          </div>
        </div>
      ) 
    };
    export default Chat;
Enter fullscreen mode Exit fullscreen mode

Here is how our chat app looks.

Chat App

Encryption

Cryptr requires a secret key to encrypt or decrypt a string. At the root of our project, we create a .env.local file that will contain our secret key.

    ## .env.local

    NEXT_PUBLIC_KEY = "********"
Enter fullscreen mode Exit fullscreen mode

Using the "NEXT_PUBLIC" prefix when storing our secret key allows the environment variable to be available in our component.

Next, we import the crypt library in our chat.jsx file.

    import {  useEffect, useState } from 'react';
    import { client} from '../init' 
    import { Databases } from 'appwrite';

    const Cryptr = require('cryptr');
Enter fullscreen mode Exit fullscreen mode

We then create a cryptr instance to encrypt and decrypt our string using our secret key.

    import {  useEffect, useState } from 'react';
    import { client} from '../init' 
    import { Databases } from 'appwrite';
    const Cryptr = require('cryptr');

    const Chat = () => {
      ...  
      const cryptr = new Cryptr(process.env.NEXT_PUBLIC_KEY);
      return (
        ...
       ) 
    };
Enter fullscreen mode Exit fullscreen mode

The sendMessages function in our chat.jsx file will be responsible for encrypting the data. The crypt library lets us use the encrypt function to encrypt strings in our application.

    const sendMessage = async () => {
      // encrypt the string in our message state variable
      const encryptedMessage = cryptr.encrypt(message)
       try {
        await databases.createDocument('62dc54f155f11d4c38cb', 'unique()', {
        // stores the encrypted message instead of the original message
           "message": encryptedMessage
         });
         alert('message sent')
         setMessages("")
         listMessages()
       } catch (error) {
        console.log(error)
      }
    }
Enter fullscreen mode Exit fullscreen mode

The sendMessage function encrypts the data in the message state variable and then stores the encrypted data on our Appwrite database.

Chat App

The "hey" message gets encrypted and stored in our database as a bunch of numbers.

Appwrite Databse

Next, we’ll retrieve the encrypted data from our database and decrypt it to get the original message.

In the listMessages function, we’ll now decrypt the message we obtained from the Appwrite database.

    const listMessages = async () => {
      const promise = await databases.listDocuments('62dc54f155f11d4c38cb');
      setDatabaseMessages([])
      promise.documents.map((document) =>{ 
      // map through the documents in the collection and decrypt each message
        const decryptedMessage = cryptr.decrypt(document.message)
        setDatabaseMessages(prevArray => [...prevArray, decryptedMessage])
      }
       )
    }
Enter fullscreen mode Exit fullscreen mode

The listMessages function cleans out the databaseMessages array before looping and decrypting the messages in the document.

Appwrite Database Console

Here is how our chat app should look.

Appwrite Database

Conclusion

This article discussed achieving an end-to-end encrypted chat with cryptr and Appwrite.

Resources

Top comments (4)

Collapse
 
panospan profile image
PanosK • Edited

Warning!
It is important to notice that NEXT_PUBLIC_ environment variable prefixes should only be used for values that are non-sensitive. It's not secure to store your secret encryption key on a NEXT_PUBLIC_ env variable. Consider using Next.js API routes to isolate any service-oriented business logic to the server-side of things. ( for example implement and call from client-side a route like /api/sendEncryptedMessageToDB/ and handle encryption from there (Nextjs's server-side), before sending to the client side of NextJs)
More info here

Collapse
 
fibonacid profile image
Lorenzo Rivosecchi

Thanks for the well written post.
Would it be possible to make it so that only the users can decrypt the data?
I think that for an accurate E2E encryption model, the developers should not be able to decrypt the data.

Collapse
 
amaraiheanacho profile image
Amarachi Iheanacho

okay, ill work on it and create a an article

Collapse
 
matheins profile image
matheins

This is not an end-to-end encrypted chat. Also I highly recommend to edit this article to not use a "NEXT_PUBLIC_" env variable.