refactored db and crypto folders/exports

This commit is contained in:
AtHeartEngineer
2023-09-01 17:08:23 -07:00
parent a650243350
commit ca5116871a
15 changed files with 665 additions and 629 deletions

4
src/crypto/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './rateCommitmentHasher';
export * from './shamirRecovery';
export * from './signalHash';
export * from './verifier';

View File

@@ -1,7 +1,8 @@
import { poseidon2 } from 'poseidon-lite/poseidon2';
function getRateCommitmentHash(identityCommitment: bigint, userMessageLimit: number | bigint) {
export function getRateCommitmentHash(
identityCommitment: bigint,
userMessageLimit: number | bigint
): bigint {
return poseidon2([identityCommitment, userMessageLimit]);
}
export default getRateCommitmentHash;

View File

@@ -6,7 +6,11 @@ import { calculateSignalHash } from './signalHash';
const v = new RLNVerifier(vkey);
async function verifyProof(msg: MessageI, room: RoomI, epochErrorRange = 5): Promise<boolean> {
export async function verifyProof(
room: RoomI,
msg: MessageI,
epochErrorRange = 5
): Promise<boolean> {
if (!msg.roomId || !msg.message || !msg.proof || !msg.epoch) {
console.warn('Missing required fields:', msg);
return false;
@@ -62,5 +66,3 @@ async function verifyProof(msg: MessageI, room: RoomI, epochErrorRange = 5): Pro
// Check that the proof is correct
return v.verifyProof(rlnIdentifier, proof);
}
export default verifyProof;

View File

@@ -1,451 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { PrismaClient } from '@prisma/client';
import { genId } from 'discreetly-interfaces';
import type { RoomI } from 'discreetly-interfaces';
import { serverConfig } from '../config/serverConfig';
import { genMockUsers, genClaimCodeArray, pp } from '../utils';
import getRateCommitmentHash from '../crypto/rateCommitmentHasher';
const prisma = new PrismaClient();
interface CodeStatus {
claimed: boolean;
roomIds: string[];
}
interface RoomsFromClaimCode {
roomIds: string[];
}
/**
* Gets a room by id
* @param {string} id The id of the room to get
* @returns {Promise<RoomI | null>}The room, or null if it doesn't exist
*/
export async function getRoomByID(id: string): Promise<RoomI | null> {
const room = await prisma.rooms
.findUnique({
where: {
roomId: id
},
// Filter out the information we want from the room
select: {
id: true,
roomId: true,
name: true,
identities: true,
rateLimit: true,
userMessageLimit: true,
membershipType: true,
contractAddress: true,
bandadaAddress: true,
bandadaGroupId: true,
type: true
}
})
.then((room) => {
return room;
})
.catch((err) => {
console.error(err);
throw err; // Add this line to throw the error
});
return new Promise((resolve, reject) => {
if (room) {
resolve(room as RoomI);
}
reject('Room not found');
});
}
/* TODO Need to create a system here where the client needs to provide a
proof they know the secrets to some Identity Commitment with a unix epoch
time stamp to prevent replay attacks
https://github.com/Discreetly/IdentityCommitmentNullifierCircuit <- Circuit and JS to do this
*/
/**
* This function takes in an identity and returns the rooms the identity is in.
* @param identity - the identity of a user
* @returns an array of roomIds
*/
export async function getRoomsByIdentity(identity: string): Promise<string[]> {
const r: string[] = [];
try {
const rooms = await prisma.rooms.findMany({
where: {
semaphoreIdentities: {
has: identity
}
}
});
rooms.forEach((room) => {
r.push(room.roomId);
});
return r;
} catch (err) {
console.error(err);
return [];
}
}
/**
* Finds a claim code in the database.
*
* @param {string} code - The code to find.
* @returns {Promise<CodeStatus | null>} - The claim code, if found.
*/
export function findClaimCode(code: string): Promise<CodeStatus | null> {
return prisma.claimCodes.findUnique({
where: { claimcode: code }
});
}
/**
* Update the claim_code table to mark the given code as claimed.
* @param {string} code - The code to update
* @returns {Promise<RoomsFromClaimCode>} - The rooms associated with the claim code
*/
export function updateClaimCode(code: string): Promise<RoomsFromClaimCode> {
return prisma.claimCodes.update({
where: { claimcode: code },
data: { claimed: true }
});
}
/*
The sanitizeIDC function takes a string and returns a string.
The string is converted to a BigInt and then back to a string.
If the string has no loss of precision, it is returned.
Otherwise, an error is thrown.
*/
function sanitizeIDC(idc: string): string {
try {
const tempBigInt = BigInt(idc);
const tempString = tempBigInt.toString();
if (idc === tempString) {
return idc;
} else {
throw new Error('Invalid IDC provided.');
}
} catch (error) {
throw new Error('Invalid IDC provided.');
}
}
/**
* This code updates the identity commitments of a list of rooms.
* It adds the identity commitment to the identity list of each room,
* and also adds it to the bandada of each room. The identity commitment is
* sanitized before being added to the database.
* @param idc - The identity commitment of the user
* @param roomIds - The list of roomIds that the user is in
* @returns {Promise<void>} - A promise that resolves when the update is complete
*/
export async function updateRoomIdentities(
idc: string,
roomIds: string[]
): Promise<string[] | void> {
const identityCommitment = sanitizeIDC(idc);
return await prisma.rooms
.findMany({
where: { id: { in: roomIds } }
})
.then(async (rooms) => {
const identityRooms = await addIdentityToIdentityListRooms(rooms, identityCommitment);
const bandadaRooms = await addIdentityToBandadaRooms(rooms, identityCommitment);
return [...identityRooms, ...bandadaRooms] as string[];
})
.catch((err) => {
pp(err, 'error');
});
}
/**
* Adds a user's identity commitment to the semaphoreIdentities list and adds their rate commitment to the identities list for each of the identity list rooms that they are in.
* @param {rooms} - The list of rooms that the user is in
* @param {string} identityCommitment - The user's identity commitment
* @return {string[]} addedRooms - The list of rooms that the user was added to
*/
async function addIdentityToIdentityListRooms(
rooms,
identityCommitment: string
): Promise<string[]> {
const identityListRooms = rooms
.filter(
(room: RoomI) =>
room.membershipType === 'IDENTITY_LIST' &&
!room.semaphoreIdentities?.includes(identityCommitment)
)
.map((room) => room.id as string);
const addedRooms: string[] = [];
const promises = identityListRooms.map(async (roomId) => {
const room = rooms.find((r) => r.id === roomId);
if (room) {
try {
await prisma.rooms.update({
where: { id: roomId },
data: {
identities: {
push: getRateCommitmentHash(
BigInt(identityCommitment),
BigInt((room.userMessageLimit as number) ?? 1)
).toString()
},
semaphoreIdentities: { push: identityCommitment }
}
});
console.debug(`Successfully added user to Identity List room ${room.roomId}`);
addedRooms.push(roomId as string);
} catch (err) {
console.error(err);
}
}
});
await Promise.all(promises);
return addedRooms;
}
/**
* This code adds a new identity commitment to the list of identities in a bandada room.
* First we get the list of bandada rooms that contain the identity commitment.
* Then we iterate over the list of rooms and add the identity commitment to each room.
* After that we update the list of identities in each room in the database.
* Finally, we send a POST request to the bandada server to add the identity to the group.
* @param {RoomI[]} rooms - The list of rooms that contain the identity commitment.
* @param {string} identityCommitment - The identity commitment to be added to the bandada room.
* @return {string[]} addedRooms - The list of rooms that the user was added to
*/
async function addIdentityToBandadaRooms(rooms, identityCommitment: string): Promise<string[]> {
const bandadaGroupRooms = rooms
.filter(
(room: RoomI) =>
room.membershipType === 'BANDADA_GROUP' &&
!room.semaphoreIdentities?.includes(identityCommitment)
)
.map((room) => room as RoomI);
const addedRooms: string[] = [];
if (bandadaGroupRooms.length > 0) {
const promises = bandadaGroupRooms.map(async (room) => {
const rateCommitment = getRateCommitmentHash(
BigInt(identityCommitment),
BigInt((room.userMessageLimit as number) ?? 1)
).toString();
if (!room.bandadaAPIKey) {
console.error('API key is missing for room:', room);
return;
}
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': room.bandadaAPIKey
}
};
try {
await prisma.rooms.update({
where: { id: room.id },
data: {
identities: {
push: rateCommitment
},
semaphoreIdentities: { push: identityCommitment }
}
});
const url = `https://${room.bandadaAddress}/groups/${room.bandadaGroupId}/members/${rateCommitment}`;
const response = await fetch(url, requestOptions);
console.log(response);
if (response.status == 201) {
console.debug(`Successfully added user to Bandada group ${room.bandadaAddress}`);
addedRooms.push(room.id as string);
}
} catch (err) {
console.error(err);
}
});
await Promise.all(promises);
}
return addedRooms;
}
/**
* This function is used to find rooms that have been updated
* It is used in the findUpdatedRooms function
* It is important because it allows the user to see which rooms have been updated
* @param {string[]} roomIds - The list of roomIds that the user is in
* @returns {Promise<RoomI[]>} - A promise that resolves to a list of rooms
*/
export async function findUpdatedRooms(roomIds: string[]): Promise<RoomI[]> {
const rooms = await prisma.rooms.findMany({
where: { id: { in: roomIds } }
});
return new Promise((resolve, reject) => {
if (rooms) {
resolve(rooms as RoomI[]);
}
reject('No rooms found');
});
}
/**
* This function creates a system message in a room.
* The message will be the same in all rooms if no roomId is passed.
* If a roomId is passed, the message will be created in that room.
* @param {string} message - The message to be created
* @param {string} roomId - The roomId to create the message in
*/
export function createSystemMessages(message: string, roomId?: string): Promise<unknown> {
const query = roomId ? { where: { roomId } } : undefined;
return prisma.rooms
.findMany(query)
.then((rooms) => {
if (roomId && rooms.length === 0) {
return Promise.reject('Room not found');
}
const createMessages = rooms.map((room) => {
return prisma.messages.create({
data: {
message,
roomId: room.roomId,
messageId: '0',
proof: JSON.stringify({})
}
});
});
return Promise.all(createMessages);
})
.catch((err) => {
console.error(err);
return Promise.reject(err);
});
}
export interface BandadaRoom extends RoomI {
bandadaAPIKey: string;
}
/**
* This function takes in an identity and a room and removes the identity from the room
* by setting its semaphoreIdentities to 0n and identities to 0n
* @param {string} idc - The identity of the user
* @param {RoomI} room - The room to remove the identity from
* @returns {Promise<void | RoomI>} - A promise that resolves to the room
*/
export function removeIdentityFromRoom(idc: string, room: RoomI): Promise<void | RoomI> {
const updateSemaphoreIdentities =
room.semaphoreIdentities?.map((identity) => (identity === idc ? '0' : (identity as string))) ??
[];
const rateCommitmentsToUpdate = getRateCommitmentHash(
BigInt(idc),
BigInt(room.userMessageLimit!)
).toString();
const updatedRateCommitments =
room.identities?.map((limiter) =>
limiter == rateCommitmentsToUpdate ? '0' : (limiter as string)
) ?? [];
return prisma.rooms
.update({
where: { id: room.id },
data: {
identities: updatedRateCommitments,
semaphoreIdentities: updateSemaphoreIdentities
}
})
.then((room) => {
return room as RoomI;
})
.catch((err) => {
console.error(err);
});
}
/**
* Creates a new room with the given name and optional parameters.
* @param {string} name - The name of the room.
* @param {number} [rateLimit=1000] - The length of an epoch in milliseconds
* @param {number} [userMessageLimit=1] - The message limit per user per epoch
* @param {number} [numClaimCodes=0] - The number of claim codes to generate for the room.
* @param {number} [approxNumMockUsers=20] - The approximate number of mock users to generate for the room.
* @param {string} [type='IDENTITY_LIST'] - The type of room to create.
* @param {string} [bandadaAddress] - The address of the bandada server.
* @param {string} [bandadaGroupId] - The id of the bandada group.
* @param {string} [bandadaAPIKey] - The API key for the bandada server.
* @param {string} [membershipType] - The membership type of the room.
* @returns {Promise<boolean>} - A promise that resolves to true if the room was created successfully.
*/
export async function createRoom(
roomName: string,
rateLimit = 1000,
userMessageLimit = 1,
numClaimCodes = 0,
approxNumMockUsers = 20,
type: string,
bandadaAddress?: string,
bandadaGroupId?: string,
bandadaAPIKey?: string,
membershipType?: string
): Promise<boolean> {
const claimCodes: { claimcode: string }[] = genClaimCodeArray(numClaimCodes);
const mockUsers: string[] = genMockUsers(approxNumMockUsers);
const identityCommitments: string[] = mockUsers.map((user) =>
getRateCommitmentHash(BigInt(user), BigInt(userMessageLimit)).toString()
);
const roomData = {
where: {
roomId: genId(serverConfig.id as bigint, roomName).toString()
},
update: {},
create: {
roomId: genId(serverConfig.id as bigint, roomName).toString(),
name: roomName,
rateLimit: rateLimit,
userMessageLimit: userMessageLimit,
semaphoreIdentities: mockUsers,
identities: identityCommitments,
type,
bandadaAddress,
bandadaGroupId,
bandadaAPIKey,
membershipType,
claimCodes: {
create: claimCodes
}
}
};
return await prisma.rooms
.upsert(roomData)
.then(() => {
return true;
})
.catch((err) => {
console.error(err);
return false;
});
}

137
src/data/db/create.ts Normal file
View File

@@ -0,0 +1,137 @@
import { PrismaClient } from '@prisma/client';
import { getRateCommitmentHash, genId, MessageI } from 'discreetly-interfaces';
import { serverConfig } from '../../config/serverConfig';
import { genClaimCodeArray, genMockUsers } from '../../utils';
const prisma = new PrismaClient();
/**
* Creates a new room with the given name and optional parameters.
* @param {string} name - The name of the room.
* @param {number} [rateLimit=1000] - The length of an epoch in milliseconds
* @param {number} [userMessageLimit=1] - The message limit per user per epoch
* @param {number} [numClaimCodes=0] - The number of claim codes to generate for the room.
* @param {number} [approxNumMockUsers=20] - The approximate number of mock users to generate for the room.
* @param {string} [type='IDENTITY_LIST'] - The type of room to create.
* @param {string} [bandadaAddress] - The address of the bandada server.
* @param {string} [bandadaGroupId] - The id of the bandada group.
* @param {string} [bandadaAPIKey] - The API key for the bandada server.
* @param {string} [membershipType] - The membership type of the room.
* @returns {Promise<boolean>} - A promise that resolves to true if the room was created successfully.
*/
export async function createRoom(
roomName: string,
rateLimit = 1000,
userMessageLimit = 1,
numClaimCodes = 0,
approxNumMockUsers = 20,
type: string,
bandadaAddress?: string,
bandadaGroupId?: string,
bandadaAPIKey?: string,
membershipType?: string
): Promise<boolean> {
const claimCodes: { claimcode: string }[] = genClaimCodeArray(numClaimCodes);
const mockUsers: string[] = genMockUsers(approxNumMockUsers);
const identityCommitments: string[] = mockUsers.map((user) =>
getRateCommitmentHash(BigInt(user), BigInt(userMessageLimit)).toString()
);
const roomData = {
where: {
roomId: genId(serverConfig.id as bigint, roomName).toString()
},
update: {},
create: {
roomId: genId(serverConfig.id as bigint, roomName).toString(),
name: roomName,
rateLimit: rateLimit,
userMessageLimit: userMessageLimit,
semaphoreIdentities: mockUsers,
identities: identityCommitments,
type,
bandadaAddress,
bandadaGroupId,
bandadaAPIKey,
membershipType,
claimCodes: {
create: claimCodes
}
}
};
return await prisma.rooms
.upsert(roomData)
.then(() => {
return true;
})
.catch((err) => {
console.error(err);
return false;
});
}
/**
* This function creates a system message in a room.
* The message will be the same in all rooms if no roomId is passed.
* If a roomId is passed, the message will be created in that room.
* @param {string} message - The message to be created
* @param {string} roomId - The roomId to create the message in
*/
export function createSystemMessages(message: string, roomId?: string): Promise<unknown> {
const query = roomId ? { where: { roomId } } : undefined;
return prisma.rooms
.findMany(query)
.then((rooms) => {
if (roomId && rooms.length === 0) {
return Promise.reject('Room not found');
}
const createMessages = rooms.map((room) => {
return prisma.messages.create({
data: {
message,
roomId: room.roomId,
messageId: '0',
proof: JSON.stringify({})
}
});
});
return Promise.all(createMessages);
})
.catch((err) => {
console.error(err);
return Promise.reject(err);
});
}
/**
* Adds a message to a room.
* @param {string} roomId - The ID of the room to add the message to.
* @param {MessageI} message - The message to add to the room.
* @returns {Promise<unknown>} - A promise that resolves when the message has been added to the room.
*/
export function createMessageInRoom(roomId: string, message: MessageI): Promise<unknown> {
if (!message.epoch) {
throw new Error('Epoch not provided');
}
return prisma.rooms.update({
where: {
roomId: roomId
},
data: {
epochs: {
create: {
epoch: String(message.epoch),
messages: {
create: {
message: message.message ? String(message.message) : '',
messageId: message.messageId ? message.messageId.toString() : '',
proof: JSON.stringify(message.proof),
roomId: roomId
}
}
}
}
}
});
}

135
src/data/db/find.ts Normal file
View File

@@ -0,0 +1,135 @@
import { PrismaClient } from '@prisma/client';
import { MessageI, RoomI } from 'discreetly-interfaces';
import { CodeStatus } from '../../types/';
const prisma = new PrismaClient();
/**
* Gets a room by id
* @param {string} id The id of the room to get
* @returns {Promise<RoomI | null>}The room, or null if it doesn't exist
*/
export async function findRoomById(id: string): Promise<RoomI | null> {
const room = await prisma.rooms
.findUnique({
where: {
roomId: id
},
// Filter out the information we want from the room
select: {
id: true,
roomId: true,
name: true,
identities: true,
rateLimit: true,
userMessageLimit: true,
membershipType: true,
contractAddress: true,
bandadaAddress: true,
bandadaGroupId: true,
type: true
}
})
.then((room) => {
return room;
})
.catch((err) => {
console.error(err);
throw err; // Add this line to throw the error
});
return new Promise((resolve, reject) => {
if (room) {
resolve(room as RoomI);
}
reject('Room not found');
});
}
/* TODO Need to create a system here where the client needs to provide a
proof they know the secrets to some Identity Commitment with a unix epoch
time stamp to prevent replay attacks
https://github.com/Discreetly/IdentityCommitmentNullifierCircuit <- Circuit and JS to do this
*/
/**
* This function takes in an identity and returns the rooms the identity is in.
* @param identity - the identity of a user
* @returns an array of roomIds
*/
export async function findRoomsByIdentity(identity: string): Promise<string[]> {
const r: string[] = [];
try {
const rooms = await prisma.rooms.findMany({
where: {
semaphoreIdentities: {
has: identity
}
}
});
rooms.forEach((room) => {
r.push(room.roomId);
});
return r;
} catch (err) {
console.error(err);
return [];
}
}
/**
* Finds a claim code in the database.
*
* @param {string} code - The code to find.
* @returns {Promise<CodeStatus | null>} - The claim code, if found.
*/
export function findClaimCode(code: string): Promise<CodeStatus | null> {
return prisma.claimCodes.findUnique({
where: { claimcode: code }
});
}
/**
* This function is used to find rooms that have been updated
* It is used in the findUpdatedRooms function
* It is important because it allows the user to see which rooms have been updated
* @param {string[]} roomIds - The list of roomIds that the user is in
* @returns {Promise<RoomI[]>} - A promise that resolves to a list of rooms
*/
export async function findUpdatedRooms(roomIds: string[]): Promise<RoomI[]> {
const rooms = await prisma.rooms.findMany({
where: { id: { in: roomIds } }
});
return new Promise((resolve, reject) => {
if (rooms) {
resolve(rooms as RoomI[]);
}
reject('No rooms found');
});
}
export async function findRoomWithMessageId(
roomId: string,
message: MessageI
): Promise<MessageI | null> {
try {
const room = await prisma.rooms.findFirst({
where: { roomId },
include: {
epochs: {
where: { epoch: String(message.epoch) },
include: {
messages: {
where: { messageId: message.messageId }
}
}
}
}
});
if (!room) {
return null;
}
return room.epochs[0].messages[0];
} catch (err) {
console.error(err);
throw err;
}
}

4
src/data/db/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './create';
export * from './find';
export * from './update';
export * from './remove';

51
src/data/db/remove.ts Normal file
View File

@@ -0,0 +1,51 @@
import { PrismaClient } from '@prisma/client';
import { RoomI, getRateCommitmentHash } from 'discreetly-interfaces';
const prisma = new PrismaClient();
/**
* This function takes in an identity and a room and removes the identity from the room
* by setting its semaphoreIdentities to 0n and identities to 0n
* @param {string} idc - The identity of the user
* @param {RoomI} room - The room to remove the identity from
* @returns {Promise<void | RoomI>} - A promise that resolves to the room
*/
export function removeIdentityFromRoom(idc: string, room: RoomI): Promise<void | RoomI> {
const updateSemaphoreIdentities =
room.semaphoreIdentities?.map((identity) => (identity === idc ? '0' : (identity as string))) ??
[];
const rateCommitmentsToUpdate = getRateCommitmentHash(
BigInt(idc),
BigInt(room.userMessageLimit!)
).toString();
const updatedRateCommitments =
room.identities?.map((limiter) =>
limiter == rateCommitmentsToUpdate ? '0' : (limiter as string)
) ?? [];
return prisma.rooms
.update({
where: { id: room.id },
data: {
identities: updatedRateCommitments,
semaphoreIdentities: updateSemaphoreIdentities
}
})
.then((room) => {
return room as RoomI;
})
.catch((err) => {
console.error(err);
});
}
export function removeRoom(roomId: string) {
console.warn('removeRoom not implemented', roomId);
//TODO removeRoom function
}
export function removeMessage(roomId: string, messageId: string) {
console.warn('removeMessage not implemented', roomId, messageId);
//TODO removeMessage function
}

174
src/data/db/update.ts Normal file
View File

@@ -0,0 +1,174 @@
import { PrismaClient } from '@prisma/client';
import { sanitizeIDC } from '../utils';
import { RoomI } from 'discreetly-interfaces';
import { getRateCommitmentHash } from '../../crypto';
import { pp } from '../../utils';
import { RoomWithSecretsI, RoomsFromClaimCode } from '../../types/';
const prisma = new PrismaClient();
/**
* This code updates the identity commitments of a list of rooms.
* It adds the identity commitment to the identity list of each room,
* and also adds it to the bandada of each room. The identity commitment is
* sanitized before being added to the database.
* @param idc - The identity commitment of the user
* @param roomIds - The list of roomIds that the user is in
* @returns {Promise<void>} - A promise that resolves when the update is complete
*/
export async function updateRoomIdentities(
idc: string,
roomIds: string[]
): Promise<string[] | void> {
try {
const identityCommitment: string = sanitizeIDC(idc);
const rooms: RoomWithSecretsI[] | null = (await prisma.rooms.findMany({
where: { id: { in: roomIds } }
})) as RoomWithSecretsI[];
if (!rooms) {
throw new Error('No rooms found with the provided IDs');
}
const identityRooms: string[] = await addIdentityToIdentityListRooms(rooms, identityCommitment);
const bandadaRooms: string[] = await addIdentityToBandadaRooms(rooms, identityCommitment);
return [...identityRooms, ...bandadaRooms];
} catch (err) {
pp(err, 'error');
throw err;
}
}
/**
* Update the claim_code table to mark the given code as claimed.
* @param {string} code - The code to update
* @returns {Promise<RoomsFromClaimCode>} - The rooms associated with the claim code
*/
export function updateClaimCode(code: string): Promise<RoomsFromClaimCode> {
return prisma.claimCodes.update({
where: { claimcode: code },
data: { claimed: true }
});
}
/**
* Adds a user's identity commitment to the semaphoreIdentities list and adds their rate commitment to the identities list for each of the identity list rooms that they are in.
* @param {rooms} - The list of rooms that the user is in
* @param {string} identityCommitment - The user's identity commitment
* @return {string[]} addedRooms - The list of rooms that the user was added to
*/
export async function addIdentityToIdentityListRooms(
rooms: RoomI[] | RoomWithSecretsI[],
identityCommitment: string
): Promise<string[]> {
const identityListRooms = rooms
.filter(
(room: RoomI) =>
room.membershipType === 'IDENTITY_LIST' &&
!room.semaphoreIdentities?.includes(identityCommitment)
)
.map((room) => room.roomId as string);
const addedRooms: string[] = [];
const promises = identityListRooms.map(async (roomId) => {
const room = rooms.find((r) => r.roomId === roomId);
if (room) {
try {
await prisma.rooms.update({
where: { id: roomId },
data: {
identities: {
push: getRateCommitmentHash(
BigInt(identityCommitment),
BigInt(room.userMessageLimit! ?? 1)
).toString()
},
semaphoreIdentities: { push: identityCommitment }
}
});
console.debug(`Successfully added user to Identity List room ${room.roomId}`);
addedRooms.push(roomId);
} catch (err) {
console.error(err);
}
}
});
await Promise.all(promises);
return addedRooms;
}
/**
* This code adds a new identity commitment to the list of identities in a bandada room.
* First we get the list of bandada rooms that contain the identity commitment.
* Then we iterate over the list of rooms and add the identity commitment to each room.
* After that we update the list of identities in each room in the database.
* Finally, we send a POST request to the bandada server to add the identity to the group.
* @param {RoomI[]} rooms - The list of rooms that contain the identity commitment.
* @param {string} identityCommitment - The identity commitment to be added to the bandada room.
* @return {string[]} addedRooms - The list of rooms that the user was added to
*/
export async function addIdentityToBandadaRooms(
rooms: RoomWithSecretsI[],
identityCommitment: string
): Promise<string[]> {
const bandadaGroupRooms = rooms
.filter(
(room: RoomI) =>
room.membershipType === 'BANDADA_GROUP' &&
!room.semaphoreIdentities?.includes(identityCommitment)
)
.map((room) => room);
const addedRooms: string[] = [];
if (bandadaGroupRooms.length > 0) {
const promises = bandadaGroupRooms.map(async (room) => {
const rateCommitment = getRateCommitmentHash(
BigInt(identityCommitment),
BigInt(room.userMessageLimit! ?? 1)
).toString();
if (!room.bandadaAPIKey) {
console.error('API key is missing for room:', room);
return;
}
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': room.bandadaAPIKey
}
};
try {
await prisma.rooms.update({
where: { id: room.id },
data: {
identities: {
push: rateCommitment
},
semaphoreIdentities: { push: identityCommitment }
}
});
const url = `https://${room.bandadaAddress}/groups/${room.bandadaGroupId}/members/${rateCommitment}`;
const response = await fetch(url, requestOptions);
console.log(response);
if (response.status == 201) {
console.debug(`Successfully added user to Bandada group ${room.bandadaAddress}`);
addedRooms.push(room.id);
}
} catch (err) {
console.error(err);
}
});
await Promise.all(promises);
}
return addedRooms;
}

View File

@@ -1,11 +1,8 @@
import { removeIdentityFromRoom } from './db';
import { PrismaClient } from '@prisma/client';
import { createMessageInRoom, findRoomWithMessageId, removeIdentityFromRoom } from './db/';
import { MessageI, RoomI } from 'discreetly-interfaces';
import { shamirRecovery, getIdentityCommitmentFromSecret } from '../crypto/shamirRecovery';
import { RLNFullProof } from 'rlnjs';
import verifyProof from '../crypto/verifier';
const prisma = new PrismaClient();
import { verifyProof } from '../crypto/';
interface CollisionCheckResult {
collision: boolean;
@@ -22,98 +19,46 @@ interface CollisionCheckResult {
*/
async function checkRLNCollision(roomId: string, message: MessageI): Promise<CollisionCheckResult> {
return new Promise((res) => {
prisma.rooms
.findFirst({
where: { roomId },
include: {
epochs: {
where: { epoch: String(message.epoch) },
include: {
messages: {
where: { messageId: message.messageId }
}
}
}
}
})
.then((oldMessage) => {
if (!message.proof) {
throw new Error('Proof not provided');
}
const oldMessage: MessageI | null = await findRoomWithMessageId(roomId, message);
if (
!oldMessage ||
!oldMessage?.epochs[0]?.messages ||
!oldMessage?.epochs[0]?.messages[0] ||
!oldMessage?.epochs[0]?.messages[0]?.proof
) {
console.debug('No collision', oldMessage);
res({ collision: false } as CollisionCheckResult);
} else {
const oldMessageProof = JSON.parse(
oldMessage.epochs[0].messages[0].proof
) as RLNFullProof;
const oldMessagex2 = BigInt(oldMessageProof.snarkProof.publicSignals.x);
const oldMessagey2 = BigInt(oldMessageProof.snarkProof.publicSignals.y);
let proof: RLNFullProof;
if (typeof message.proof === 'string') {
proof = JSON.parse(message.proof) as RLNFullProof;
} else {
proof = message.proof;
}
const [x1, y1] = [
BigInt(proof.snarkProof.publicSignals.x),
BigInt(proof.snarkProof.publicSignals.y)
];
const [x2, y2] = [oldMessagex2, oldMessagey2];
const secret = shamirRecovery(x1, x2, y1, y2);
res({
collision: true,
secret,
oldMessage: oldMessage.epochs[0].messages[0] as unknown as MessageI
} as CollisionCheckResult);
}
})
.catch((err) => console.error(err));
});
}
/**
* Adds a message to a room.
* @param {string} roomId - The ID of the room to add the message to.
* @param {MessageI} message - The message to add to the room.
* @returns {Promise<unknown>} - A promise that resolves when the message has been added to the room.
*/
function addMessageToRoom(roomId: string, message: MessageI): Promise<unknown> {
if (!message.epoch) {
throw new Error('Epoch not provided');
if (!message.proof) {
throw new Error('Proof not provided');
}
return prisma.rooms.update({
where: {
roomId: roomId
},
data: {
epochs: {
create: {
epoch: String(message.epoch),
messages: {
create: {
message: message.message ? String(message.message) : '',
messageId: message.messageId ? message.messageId.toString() : '',
proof: JSON.stringify(message.proof),
roomId: roomId
}
}
}
}
if (!oldMessage?.proof) {
console.debug('No collision', oldMessage);
return { collision: false } as CollisionCheckResult;
} else {
let oldMessageProof: RLNFullProof;
if (typeof oldMessage.proof === 'string') {
oldMessageProof = JSON.parse(oldMessage.proof) as RLNFullProof;
} else {
oldMessageProof = oldMessage.proof;
}
});
const oldMessagex2 = BigInt(oldMessageProof.snarkProof.publicSignals.x);
const oldMessagey2 = BigInt(oldMessageProof.snarkProof.publicSignals.y);
let proof: RLNFullProof;
if (typeof message.proof === 'string') {
proof = JSON.parse(message.proof) as RLNFullProof;
} else {
proof = message.proof;
}
const [x1, y1] = [
BigInt(proof.snarkProof.publicSignals.x),
BigInt(proof.snarkProof.publicSignals.y)
];
const [x2, y2] = [oldMessagex2, oldMessagey2];
const secret = shamirRecovery(x1, x2, y1, y2);
return {
collision: true,
secret,
oldMessage: oldMessage
} as CollisionCheckResult;
}
}
export interface validateMessageResult {
@@ -130,7 +75,7 @@ async function handleCollision(
const roomId = room.roomId.toString();
if (!collisionResult.collision) {
try {
await addMessageToRoom(roomId, message);
await createMessageInRoom(roomId, message);
console.debug(
`Message added to room: ${
typeof message.message === 'string'
@@ -167,7 +112,7 @@ export async function validateMessage(
message: MessageI
): Promise<validateMessageResult> {
const roomId = room.roomId.toString();
const validProof = await verifyProof(message, room);
const validProof: boolean = await verifyProof(room, message);
if (validProof) {
const collisionResult = await checkRLNCollision(roomId, message);
const result = await handleCollision(room, message, collisionResult);

23
src/data/utils.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* The sanitizeIDC function takes a string and returns a string.
* The string is converted to a BigInt and then back to a string.
* If the string has no loss of precision, it is returned.
* Otherwise, an error is thrown.
*
* @param {string} idc - The string to be sanitized.
* @returns {string} - The sanitized string if it has no loss of precision.
* @throws {Error} - Throws an error if the string cannot be converted to a BigInt or if it loses precision.
*/
export function sanitizeIDC(idc: string): string {
try {
const tempBigInt = BigInt(idc);
const tempString = tempBigInt.toString();
if (idc === tempString) {
return idc;
} else {
throw new Error('Invalid IDC provided.');
}
} catch (error) {
throw new Error('Invalid IDC provided.');
}
}

View File

@@ -3,15 +3,15 @@ import { PrismaClient } from '@prisma/client';
import { serverConfig } from '../config/serverConfig';
import { genClaimCodeArray, pp } from '../utils';
import {
getRoomByID,
getRoomsByIdentity,
findRoomById,
findRoomsByIdentity,
findClaimCode,
updateClaimCode,
updateRoomIdentities,
findUpdatedRooms,
createRoom,
createSystemMessages
} from '../data/db';
} from '../data/db/';
import { MessageI, RoomI } from 'discreetly-interfaces';
import { RLNFullProof } from 'rlnjs';
@@ -47,14 +47,11 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
} else {
const requestRoomId = req.params.id ?? '0';
pp(String('Express: fetching room info for ' + req.params.id));
getRoomByID(requestRoomId)
findRoomById(requestRoomId)
.then((room: RoomI) => {
if (!room) {
// This is set as a timeout to prevent someone from trying to brute force room ids
setTimeout(
() => res.status(500).json({ error: 'Internal Server Error' }),
1000
);
setTimeout(() => res.status(500).json({ error: 'Internal Server Error' }), 1000);
} else {
const {
roomId,
@@ -103,7 +100,7 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
['/rooms/:idc', '/api/rooms/:idc'],
asyncHandler(async (req: Request, res: Response) => {
try {
res.status(200).json(await getRoomsByIdentity(req.params.idc));
res.status(200).json(await findRoomsByIdentity(req.params.idc));
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
@@ -135,9 +132,7 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
const parsedBody: JoinData = req.body as JoinData;
if (!parsedBody.code || !parsedBody.idc) {
res
.status(400)
.json({ message: '{code: string, idc: string} expected' });
res.status(400).json({ message: '{code: string, idc: string} expected' });
}
const { code, idc } = parsedBody;
console.debug('Invite Code:', code);
@@ -162,7 +157,9 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
roomIds: updatedRooms.map((room: RoomI) => room.roomId)
});
} else {
res.status(400).json({ message: `No rooms found or identity already exists in ${String(roomIds)}` });
res
.status(400)
.json({ message: `No rooms found or identity already exists in ${String(roomIds)}` });
}
})
);
@@ -320,30 +317,33 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
return await prisma.rooms.findMany(query).then((rooms) => {
const roomIds = rooms.map((room) => room.id);
const createCodes = codes.map((code) => {
return prisma.claimCodes.create({
data: {
claimcode: code.claimcode,
claimed: false,
roomIds: roomIds
}
}).then((newCode) => {
const updatePromises = rooms.map((room) => {
return prisma.rooms.update({
where: {
roomId: room.roomId
},
data: {
claimCodeIds: {
push: newCode.id
return prisma.claimCodes
.create({
data: {
claimcode: code.claimcode,
claimed: false,
roomIds: roomIds
}
})
.then((newCode) => {
const updatePromises = rooms.map((room) => {
return prisma.rooms.update({
where: {
roomId: room.roomId
},
data: {
claimCodeIds: {
push: newCode.id
}
}
}
});
});
return Promise.all(updatePromises);
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
return Promise.all(updatePromises);
}).catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
return Promise.all(createCodes)
@@ -368,50 +368,46 @@ export function initEndpoints(app: Express, adminAuth: RequestHandler) {
* }
*/
app.post(
['/room/:roomId/addcode', '/api/room/:roomId/addcode'],
adminAuth,
(req, res) => {
const { roomId } = req.params;
const { numCodes } = req.body as { numCodes: number };
const codes = genClaimCodeArray(numCodes);
app.post(['/room/:roomId/addcode', '/api/room/:roomId/addcode'], adminAuth, (req, res) => {
const { roomId } = req.params;
const { numCodes } = req.body as { numCodes: number };
const codes = genClaimCodeArray(numCodes);
prisma.rooms
.findUnique({
where: { roomId: roomId },
include: { claimCodes: true }
})
.then((room) => {
if (!room) {
res.status(404).json({ error: 'Room not found' });
return;
}
// Map over the codes array and create a claim code for each code
const createCodes = codes.map((code) => {
return prisma.claimCodes.create({
data: {
claimcode: code.claimcode,
claimed: false,
rooms: {
connect: {
roomId: roomId
}
prisma.rooms
.findUnique({
where: { roomId: roomId },
include: { claimCodes: true }
})
.then((room) => {
if (!room) {
res.status(404).json({ error: 'Room not found' });
return;
}
// Map over the codes array and create a claim code for each code
const createCodes = codes.map((code) => {
return prisma.claimCodes.create({
data: {
claimcode: code.claimcode,
claimed: false,
rooms: {
connect: {
roomId: roomId
}
}
});
}
});
return Promise.all(createCodes);
})
.then(() => {
res.status(200).json({ message: 'Claim codes added successfully' });
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
}
);
return Promise.all(createCodes);
})
.then(() => {
res.status(200).json({ message: 'Claim codes added successfully' });
})
.catch((err) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
// This code fetches the claim codes from the database.

View File

@@ -1 +1,16 @@
import { RoomI } from 'discreetly-interfaces';
export interface CodeStatus {
claimed: boolean;
roomIds: string[];
}
export interface RoomsFromClaimCode {
roomIds: string[];
}
export interface RoomWithSecretsI extends RoomI {
bandadaAPIKey: string;
}
export type userCountI = Record<string, number>;

View File

@@ -1,6 +1,6 @@
import { MessageI, RoomI } from 'discreetly-interfaces';
import { Socket, Server as SocketIOServer } from 'socket.io';
import { getRoomByID, createSystemMessages } from '../data/db';
import { findRoomById, createSystemMessages } from '../data/db/';
import { pp } from '../utils';
import { validateMessage } from '../data/messages';
import type { validateMessageResult } from '../data/messages';
@@ -12,7 +12,7 @@ export function websocketSetup(io: SocketIOServer) {
socket.on('validateMessage', async (msg: MessageI) => {
try {
const room: RoomI | null = await getRoomByID(String(msg.roomId));
const room: RoomI | null = await findRoomById(String(msg.roomId));
if (!room) {
pp('INVALID ROOM', 'warn');
return;

View File

@@ -36,7 +36,7 @@ const testIdentity = randBigint();
const username = 'admin';
const password = process.env.PASSWORD;
describe('Endpoints should all work hopefully', () => {
describe('Endpoints should all work', () => {
test('It should respond with server info', async () => {
await request(_app)
.get('/')