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
Creating collection and attributes
On the left side of the Appwrite Console dashboard, click on the Database tab.
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.
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.
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.
Let’s create a string attribute of message with a size of 256 bits.
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';
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"])
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');
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]))
}
The listMessages
in the code block above does the following:
- Lists all the documents in a collection using Appwrite’s
listDocuments
method. ThislistDocuments
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)
}
}
The sendMessage
function above does the following:
- Creates a new document using Appwrite’s
createDocument()
function. ThiscreateDocument()
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 thelistMessages()
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>
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>
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;
Here is how our chat app looks.
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 = "********"
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');
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 (
...
)
};
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)
}
}
The sendMessage
function encrypts the data in the message
state variable and then stores the encrypted data on our Appwrite database.
The "hey" message gets encrypted and stored in our database as a bunch of numbers.
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])
}
)
}
The listMessages
function cleans out the databaseMessages
array before looping and decrypting the messages in the document.
Here is how our chat app should look.
Conclusion
This article discussed achieving an end-to-end encrypted chat with cryptr and Appwrite.
Top comments (4)
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
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.
okay, ill work on it and create a an article
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.