An app that allows you encrypt a message and share it with a link.
My goal was to create a simple way to share a private message via a URL. And in the safest possible way
The encryption process is done in the browser, using the Web Crypto API.
- A random password is generated (as a uuidv4) and converted to binary format
- Salt: A random 16-byte value ensures unique encryption, even with the same password
- Key Derivation: A secure key is created from the password, salt, 100,000 iterations, and SHA-256
- AES Key: The derived key is turned into an encryption key for AES-GCM
- Initialization Vector (IV): A 12-byte random value ensures secure encryption.
- AES-GCM: Encrypts the message using the derived key and IV.
- Output: The function returns the encrypted message (ciphertext), IV, and salt for decryption.
Once the ciphertext, the salt and the iv value have been generated, they are stored in a postgress database (using Supabase).
The ID of the entry is then packed together with the password into a base64 string, which this is then the hash of the link to be sent.
When the link is opened, the hash is decoded, and the password and the ID are extracted. After the confirmation of the user, the encrypted message is fetched (and deleted) from the database.
The password is then used to decrypt the message, and the result is displayed to the user.
Since the password is never sent to the server, I have no way of decrypting the messages from the database.
Only the person with the link (and the password it contains) can do this.
For the backend I used Supabase, a service that provides a Postgres database and an API to interact with it.
However, this would work with pretty much any database technology, as the complex processes run directly in the browser.
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ciphertext TEXT NOT NULL,
iv TEXT NOT NULL,
salt TEXT NOT NULL,
expires TIMESTAMP NOT NULL
);
Since I am using RLS (Row Level Security) on the table and the user is anonymous, I need to use a function to insert the message..
CREATE OR REPLACE FUNCTION public.insert_message(
ciphertext TEXT,
iv TEXT,
salt TEXT,
expires TIMESTAMP
) RETURNS UUID
SECURITY DEFINER
AS $$
DECLARE
new_id UUID;
BEGIN
INSERT INTO public.messages (ciphertext, iv, salt, expires)
VALUES (ciphertext, iv, salt, expires)
RETURNING id INTO new_id;
RETURN new_id;
END;
$$ LANGUAGE plpgsql;
..and another function to then get the message by ID and delete it.
CREATE OR REPLACE FUNCTION public.get_message_by_id(
message_id UUID
) RETURNS public.messages
SECURITY DEFINER
AS $$
DECLARE
result public.messages;
BEGIN
SELECT * INTO result FROM public.messages WHERE id = message_id AND expires > NOW();
DELETE FROM public.messages WHERE id = message_id;
RETURN result;
END;
$$ LANGUAGE plpgsql;