Timelock encryption

Timelock encryption allows you to encrypt data that can only be decrypted after a certain time

Timelock encryption allows you to encrypt data that can only be decrypted after a certain time. The timelock package uses Drand to generate the encryption key.

This is a basic TimeVault implementation in Svelte.

Install timelock package

pnpm i tlock-js

Create encrypt and decrypt functions

src/lib/drand.ts - based on this file

import * as tlock from 'tlock-js';

const client = tlock.mainnetClient();
client.httpOptions = {};

export async function encrypt(plaintext: string, timestampMs: number): Promise<string> {
	const round = tlock.roundAt(timestampMs, tlock.defaultChainInfo);

	return await tlock.timelockEncrypt(round, tlock.Buffer.from(plaintext), client);
}

export async function decrypt(ciphertext: string): Promise<string> {
	try {
		const result = await tlock.timelockDecrypt(ciphertext, client);
		return result.toString();
	} catch (e: any) {
		// Convert not decryptable yet error into a more informative error with a date and time
		const earlyRegex = /decryptable at round ([0-9]+)/;
		const matches = e.message.match(earlyRegex);
		if (matches) {
			const round = parseInt(matches[1]);
			const timestampMs = tlock.roundTime(tlock.defaultChainInfo, round);
			const date = new Date(timestampMs);

			throw new Error(`This message is not decryptable until ${date.toLocaleString()} UTC`);
		}

		throw e;
	}
}

Write the UI

<script>
	import { page } from '$app/stores';
	import { encrypt, decrypt } from '$lib/drand';
	import { onMount } from 'svelte';
	let plaintext = $state('Hello, world!');
	let decryptedPromise = $state();
	let copied = $state(false);
	let decryptionDate = $state();

	onMount(() => {
		if (!$page.url.hash) return;
		const ciphertext = atob($page.url.hash.slice(1));
		decryptedPromise = decrypt(ciphertext);
	});
</script>

{#if $page.url.hash}
	<a href="/">Encrypt a message</a>
	{#await decryptedPromise}
		Loading...
	{:then decrypted}
		<p>Decrypted text:</p>
		<pre>{decrypted}</pre>
	{:catch error}
		<p>Error: {error.message}</p>
	{/await}
{:else}
	<textarea bind:value={plaintext} rows="10" cols="40"></textarea>
	<br />
	<input type="datetime-local" bind:value={decryptionDate} />
	<br />
	<button
		onclick={async () => {
			if (!decryptionDate) {
				alert('Please enter a decryption date');
				return;
			}
			const url = new URL(window.location.href);
			const date = new Date(decryptionDate);
			const unix = date.getTime();
			const ciphertext = await encrypt(plaintext, unix);
			url.hash = btoa(ciphertext);
			navigator.clipboard.writeText(url.toString());
			copied = true;
			setTimeout(() => {
				copied = false;
			}, 1000);
		}}>{copied ? 'Copied!' : 'Copy encrypted link'}</button
	>
{/if}