This commit is contained in:
2023-10-23 22:37:06 -04:00
parent bcdbab737f
commit 6f5f7d49fb
13 changed files with 220 additions and 11 deletions

19
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"date-fns": "^2.30.0",
"discreetly-interfaces": "^0.1.39",
"dompurify": "^3.0.5",
"idc-nullifier": "^0.0.4",
"libsodium-wrappers": "^0.7.11",
"marked": "^7.0.5",
"node-emoji": "^2.1.0",
@@ -4070,6 +4071,11 @@
"wasmbuilder": "0.0.16"
}
},
"node_modules/circomlib": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/circomlib/-/circomlib-2.0.5.tgz",
"integrity": "sha512-O7NQ8OS+J4eshBuoy36z/TwQU0YHw8W3zxZcs4hVwpEll3e4hDm3mgkIPqItN8FDeLEKZFK3YeT/+k8TiLF3/A=="
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -5913,6 +5919,17 @@
"node": ">=0.10.0"
}
},
"node_modules/idc-nullifier": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/idc-nullifier/-/idc-nullifier-0.0.4.tgz",
"integrity": "sha512-kQvqvagzSbrMDcwmmHqw7sF6G0GHqQLJqyec/ie3v91Bo8Sjp/zXOczbE+4b2hGHUo8CnMaCL+P+ozKPh02pKg==",
"dependencies": {
"@semaphore-protocol/identity": "^3.10.1",
"circomlib": "^2.0.5",
"poseidon-lite": "^0.2.0",
"snarkjs": "^0.7.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -10582,4 +10599,4 @@
}
}
}
}
}

View File

@@ -72,6 +72,7 @@
"date-fns": "^2.30.0",
"discreetly-interfaces": "^0.1.39",
"dompurify": "^3.0.5",
"idc-nullifier": "^0.0.4",
"libsodium-wrappers": "^0.7.11",
"marked": "^7.0.5",
"node-emoji": "^2.1.0",
@@ -83,4 +84,4 @@
"viem": "^1.16.5",
"wagmi": "^1.4.3"
}
}
}

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import { alertQueue } from '$lib/stores';
import { FileDropzone } from '@skeletonlabs/skeleton';
import Loading from '../Utils/Loading.svelte';
import { theWordRequest } from '$lib/gateways/theWord';
let files: FileList;
let loading = false;
function theWordHandler(proof: any) {
loading = true;
theWordRequest(proof)
.then(({ acceptedRoomNames, err }) => {
if (err) {
alertQueue.enqueue(err, 'error');
} else {
alertQueue.enqueue(`Accepted into ${acceptedRoomNames}`, 'success');
acceptedRoomNames = acceptedRoomNames;
}
})
.catch((err) => {
console.log(err);
alertQueue.enqueue(err, 'error');
})
.finally(() => {
loading = false;
});
}
function onChangeHandler(e: Event): void {
const f = files.item(0);
console.debug(`Proof File type detected as ${f?.type}`);
let unverifiedData: any;
if (!f) {
alertQueue.enqueue('No file selected', 'warning');
return;
}
if (f.type == 'application/json' || f.type == 'text/plain') {
f.text().then((text) => {
unverifiedData = JSON.parse(text);
theWordHandler(unverifiedData);
});
} else {
alertQueue.enqueue(
'Invalid file type, must be a JSON object in the SNARKProof format',
'warning'
);
console.warn('Invalid file type');
}
}
</script>
<div class="flex flex-col gap-3 justify-between">
<section class="px-4 pt-4">
<label class="label w-full pb-1">
<span class="h5"> Prove From File:</span>
<FileDropzone
class="px-2"
name="the-word-proof.json"
bind:files
on:change={onChangeHandler}
/></label
>
</section>
{#if loading}
<Loading />
{/if}
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import EthereumGroupGateway from '$lib/components/Gateways/EthereumGroup.svelte';
import InviteCodeGateway from '$lib/components/Gateways/InviteCode.svelte';
import TheWord from '$lib/components/Gateways/TheWord.svelte';
import SelectServer from '$lib/components/Server/SelectServer.svelte';
import Card from '$lib/components/Utils/Card.svelte';
import { configStore, serverStore } from '$lib/stores';
@@ -45,4 +46,12 @@
</svelte:fragment>
<EthereumGroupGateway />
</Card>
<Card>
<svelte:fragment slot="header">The Word</svelte:fragment>
<svelte:fragment slot="description"
>Do you know <a class="link" href="https://github.com/Mach-34/the-word/">the word</a>? Prove
it.
</svelte:fragment>
<TheWord />
</Card>
</div>

View File

@@ -0,0 +1,41 @@
import { getCommitment, updateRooms } from '$lib/utils/';
import { postTheWord } from '$lib/services/server';
import { selectedServer, configStore, alertQueue } from '$lib/stores';
import { get } from 'svelte/store';
import type { JoinResponseI, SNARKProof } from '$lib/types';
export async function theWordRequest(proof: SNARKProof) {
let acceptedRoomNames: string[] = [];
let err: string | undefined;
const server = get(selectedServer);
try {
const idc = getCommitment();
if (!idc) {
alertQueue.enqueue('No identity commitment found', 'warning');
throw new Error('No identity commitment found');
}
const result = (await postTheWord(server, {
proof,
idc
})) as JoinResponseI;
console.debug('INVITE CODE RESPONSE: ', result);
if (result.status == 'valid') {
console.debug('Updating new rooms');
acceptedRoomNames = await updateRooms(server, result.roomIds);
console.log(`Added to rooms: ${acceptedRoomNames}`);
configStore.update((store) => {
store['signUpStatus']['completedSignup'] = true;
store['signUpStatus']['inviteCode'] = '';
return store;
});
return { acceptedRoomNames, undefined };
} else if (result.status == 'already-added') {
return { acceptedRoomNames, err: 'You are already a member of this room.' };
} else {
err = 'Invalid invite code.';
}
} catch (e) {
err = String((e as Error).message);
}
return { acceptedRoomNames, err };
}

View File

@@ -47,6 +47,27 @@ export async function get(urlParts: string[] | string): Promise<object> {
throw new Error(`Failed to fetch ${url}`);
}
/**
* @description - makes a get request to the api
* @param {string[] | string} urlParts - the url parts to be joined to form the url
* @returns {object} - the response from the api
* @throws {Error} - if the request fails
*/
export async function getWithData(urlParts: string[] | string, data: object): Promise<object> {
const url = cleanURL(urlParts);
const res = await fetch(url, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(data)
});
if (res.ok) {
return res.json();
}
throw new Error(`Failed to fetch ${url}`);
}
/**
* @description - makes a get request to the api
* @param {string[] | string} urlParts - the url parts to be joined to form the url

View File

@@ -1,10 +1,25 @@
import type { MessageI, ServerI } from 'discreetly-interfaces';
import type { Invites, RoomI } from '$lib/types';
import { get, post, postAuth } from './api';
import { Prover } from 'idc-nullifier';
import type { Identity } from '@semaphore-protocol/identity';
import { get, getWithData, post, postAuth } from './api';
import { getIdentity } from '$lib/utils';
import { alertQueue } from '$lib/stores';
export async function getIdentityRoomIds(server: string, idCommitment: string): Promise<string[]> {
return get([server, `api/rooms/${idCommitment}`]) as Promise<string[]>;
const prover = new Prover();
const id = getIdentity() as unknown as Identity;
if (id) {
// Proves you know the identity secret with a timestamp so this proof can't be replayed
const proof = prover.generateProof({
identity: id,
externalNullifier: BigInt(Date.now())
});
return getWithData([server, `api/rooms/${idCommitment}`], proof) as Promise<string[]>;
} else {
alertQueue.enqueue('No identity found when fetching rooms', 'error');
return [];
}
}
export async function getRoomById(server: string, roomId: string): Promise<RoomI> {
@@ -26,6 +41,10 @@ export async function postInviteCode(serverUrl: string, data: { code: string; id
return post([serverUrl, 'join'], data);
}
export async function postTheWord(serverUrl: string, data: { proof: object; idc: string }) {
return post([serverUrl, 'theword'], data);
}
export async function getMessages(serverUrl: string, roomId: string) {
return get([serverUrl, `api/room/${roomId}/messages`]) as Promise<MessageI[]>;
}

View File

@@ -11,8 +11,9 @@ import type {
roomStoreI,
selectedRoomStoreI,
serverStoreI,
roomKeyStoreI,
keyStoreI
keyStoreI,
roomPassStoreI,
roomKeyStoreI
} from '$lib/types';
/* ------------------ Server State ------------------*/
@@ -64,7 +65,7 @@ export const pixelStore = sessionable({} as pixelStoreI, 'pixelmaps');
/**
* @description Stores the encrypted key for each room keyed by the roomId
*/
export const roomKeyStore = encryptable({} as roomKeyStoreI, 'roomKey');
export const roomPassStore = encryptable({} as roomPassStoreI, 'roomKey');
/**
* @description Derived Store: The messages of the currently selected room
@@ -95,6 +96,7 @@ export const saltStore = storable('', 'salt');
* !WARN NEVER CHANGE THE STORE TYPE OR YOU RISK EXPOSING THE KEY
*/
export const keyStore = writable({} as keyStoreI);
export const roomKeyStore = writable({} as roomKeyStoreI);
export const alertQueue = queueable([]);

View File

@@ -53,3 +53,8 @@ export interface RoomFormData {
bandadaGroupId: string | undefined;
bandadaApiKey: string | undefined;
}
export interface SNARKProof {
proof: object;
publicSignals: string[];
}

View File

@@ -55,10 +55,14 @@ export interface consoleStoreI {
};
}
export interface roomKeyStoreI {
export interface roomPassStoreI {
[key: string]: string;
}
export interface roomKeyStoreI {
[key: string]: CryptoKey;
}
export type keyStoreI = CryptoKey | undefined | null;
export interface EncryptableT<Type> extends Writable<Type> {

View File

@@ -7,7 +7,7 @@ import {
keyStore,
identityExists,
alertQueue,
roomKeyStore
roomPassStore
} from '$lib/stores';
import { encryptIdentity } from './identity';
import type { IdentityStoreI } from '$lib/types';
@@ -70,7 +70,7 @@ export async function unlockPadlock(password: string) {
// encrypted stores, so we have to trigger it manually
// when the keys are derived
identityKeyStore.read();
roomKeyStore.read();
roomPassStore.read();
});
} else {
alertQueue.enqueue('Incorrect Password', 'warning');

View File

@@ -167,6 +167,7 @@
{messageId}
{messagesLeft}
{roomRateLimit}
{onlineMembers}
/>
{#if $configStore.experience == Experiences.Chat}
{#key $currentSelectedRoom.roomId}

View File

@@ -4,6 +4,7 @@
import { currentSelectedRoom, configStore } from '$lib/stores';
import { ProgressBar } from '@skeletonlabs/skeleton';
import FullCircle from 'svelte-material-icons/Circle.svelte';
import Person from 'svelte-material-icons/Account.svelte';
import ExperienceMenu from './ExperienceMenu.svelte';
export let connected: boolean;
export let currentEpoch: number;
@@ -12,7 +13,10 @@
export let roomRateLimit: number;
export let messagesLeft: () => number;
export let messageId: number;
export let onlineMembers: string;
$: roomId = $currentSelectedRoom?.roomId!.toString();
$: encrypted = $currentSelectedRoom?.encrypted ?? false;
$: ephemeral = $currentSelectedRoom?.ephemeral ?? false;
$: roomName = $currentSelectedRoom?.name ?? 'Select Room';
$: epochLengthSeconds = roomRateLimit / 1000;
$: timeToNextEpoch = epochLengthSeconds - +timeLeftInEpoch;
@@ -31,12 +35,30 @@
{/if}
</span>
<h2 class="h5 text-secondary-800-100-token" title={roomId}>
{#if encrypted}
🔒
{/if}
{#if ephemeral}
{/if}
{roomName}
</h2>
<div class="hidden sm:block ms-2 text-xs font-mono self-center">
[{timeToNextEpoch.toFixed(1)}/{epochLengthSeconds}s]
</div>
</div>
<div class="flex flex-row">
{#if !connected}
<span
class:connected
class="flex flex-row align-middle text-sm font-mono text-secondary-300-600-token"
title="Online Users"
>
<Person />
{onlineMembers}
</span>
{/if}
</div>
<div
class="flex flex-row place-content-center"
title={`These are action points, you get ${userMessageLimit} every ${epochLengthSeconds} seconds`}