Skip to content

Commit

Permalink
Merge pull request #2 from nikgraf/locker
Browse files Browse the repository at this point in the history
implement locker
  • Loading branch information
nikgraf committed Jun 2, 2024
2 parents f6283fd + b163272 commit 3ae2567
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Users use OPAQUE to authenticate with the server. After Login the server creates
- generate keys and store them locally
- store keys on lockbox
- add invitation scheme
- add retry for locker in case write fails (invalid clock)

- store data locally (api in secsync)
- fix websocket session auth in secsync
3 changes: 2 additions & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint": "echo \"Lint not setup\" # expo lint"
},
"jest": {
"preset": "jest-expo"
"preset": "jest-expo/web"
},
"dependencies": {
"@expo/vector-icons": "^14.0.0",
Expand All @@ -18,6 +18,7 @@
"@trpc/client": "^11.0.0-rc.382",
"@trpc/react-query": "^11.0.0-rc.382",
"babel-preset-expo": "~11.0.0",
"canonicalize": "^2.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"expo": "~51.0.9",
Expand Down
22 changes: 22 additions & 0 deletions apps/app/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Link } from "expo-router";
import * as React from "react";
import { View } from "react-native";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
import { Text } from "~/components/ui/text";
import { CreateListForm } from "../components/createListForm";
import { Logout } from "../components/logout";
import { useLocker } from "../hooks/useLocker";
import { trpc } from "../utils/trpc";

const Lists: React.FC = () => {
Expand All @@ -23,6 +25,16 @@ const Lists: React.FC = () => {
refetchInterval: 5000,
});

// const lockerKey = sodium.from_base64(
// "MTcyipWZ6Kiibd5fATw55i9wyEU7KbdDoTE_MRgDR98"
// );

const { content, addDocumentKey } = useLocker(
"MTcyipWZ6Kiibd5fATw55i9wyEU7KbdDoTE_MRgDR98"
);

console.log("lockerContent", content);

return (
<View>
<Link href="/login">
Expand All @@ -38,6 +50,16 @@ const Lists: React.FC = () => {

<CreateListForm />

<Button
onPress={() => {
// random number for now
const id = Math.floor(Math.random() * 10000000).toString();
addDocumentKey(id, "123");
}}
>
<Text>GENERATE</Text>
</Button>

<div className="flex flex-col gap-2 pt-4">
{documentsQuery.data?.map((doc) => (
<Link href={`/list/${doc.id}`} key={doc.id}>
Expand Down
61 changes: 61 additions & 0 deletions apps/app/src/hooks/useLocker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useMemo } from "react";
import sodium from "react-native-libsodium";
import { decryptLocker } from "../utils/decryptLocker";
import { encryptLocker } from "../utils/encryptLocker";
import { trpc } from "../utils/trpc";

export const useLocker = (key: string) => {
const createUserLockerMutation = trpc.createUserLocker.useMutation();
const latestUserLockerQuery = trpc.getLatestUserLocker.useQuery();

const keyUint8Array = sodium.from_base64(key);

const content = useMemo(() => {
if (!latestUserLockerQuery.data) {
return null;
}

const contentString = decryptLocker(
latestUserLockerQuery.data,
keyUint8Array
);
return JSON.parse(contentString) || {}; // TODO validate schema
}, [
latestUserLockerQuery.data?.ciphertext,
latestUserLockerQuery.data?.nonce,
latestUserLockerQuery.data?.commitment,
latestUserLockerQuery.data?.clock,
]);

// RETRY in case it fails
const addDocumentKey = async (documentId: string, value: string) => {
const newContent = {
...content,
[documentId]: value,
};
const newContentString = JSON.stringify(newContent);
const newClock =
latestUserLockerQuery.data?.clock !== undefined
? latestUserLockerQuery.data.clock + 1
: 0;

const { ciphertext, nonce, commitment } = encryptLocker(
newContentString,
newClock,
keyUint8Array
);

await createUserLockerMutation.mutateAsync({
ciphertext,
nonce,
commitment,
clock: newClock,
});
await latestUserLockerQuery.refetch();
};

return {
content,
addDocumentKey,
};
};
18 changes: 18 additions & 0 deletions apps/app/src/utils/decryptLocker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { decryptLocker } from "./decryptLocker";
import { encryptLocker } from "./encryptLocker";

import * as sodium from "react-native-libsodium";

beforeAll(async () => {
await sodium.ready;
});

// test encrypting and decrypting a locker
test("encryptLocker and decryptLocker", () => {
const key = new Uint8Array(32);
const content = "hello world";
const clock = 1234;
const encryptedLocker = encryptLocker(content, clock, key);
const decryptedContent = decryptLocker(encryptedLocker, key);
expect(decryptedContent).toEqual(content);
});
51 changes: 51 additions & 0 deletions apps/app/src/utils/decryptLocker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import canonicalize from "canonicalize";
import * as sodium from "react-native-libsodium";

type EncryptedLocker = {
ciphertext: string;
nonce: string;
commitment: string;
clock: number;
};

export const decryptLocker = (
encryptedLocker: EncryptedLocker,
key: Uint8Array
) => {
const { ciphertext, nonce, commitment, clock } = encryptedLocker;

const additionalData = canonicalize({ clock });
if (!additionalData) {
throw new Error("Failed to canonicalize additional data");
}
const commitmentContent = canonicalize({
nonce,
ciphertext,
additionalData,
});
if (!commitmentContent) {
throw new Error("Failed to canonicalize commitment data");
}

const isValidCommitment = sodium.crypto_auth_verify(
sodium.from_base64(commitment),
commitmentContent,
key
);
if (!isValidCommitment) {
throw new Error("Invalid commitment");
}

const decryptedContent = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
null,
sodium.from_base64(ciphertext),
additionalData,
sodium.from_base64(nonce),
key
);
if (!decryptedContent) {
throw new Error("Failed to decrypt locker");
}

return sodium.to_string(decryptedContent);
};
43 changes: 43 additions & 0 deletions apps/app/src/utils/encryptLocker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import canonicalize from "canonicalize";
import * as sodium from "react-native-libsodium";

export const encryptLocker = (
content: string,
clock: number,
key: Uint8Array
) => {
const nonce = sodium.randombytes_buf(
sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
);
const nonceString = sodium.to_base64(nonce);
const additionalData = canonicalize({ clock });
if (!additionalData) {
throw new Error("Failed to canonicalize additional data");
}
const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
content,
additionalData,
null,
nonce,
key
);
const ciphertextString = sodium.to_base64(ciphertext);

const commitmentContent = canonicalize({
nonce: nonceString,
ciphertext: ciphertextString,
additionalData,
});
if (!commitmentContent) {
throw new Error("Failed to canonicalize commitment data");
}

const commitment = sodium.crypto_auth(commitmentContent, key);

return {
nonce: nonceString,
ciphertext: ciphertextString,
commitment: sodium.to_base64(commitment),
clock,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Warnings:
- You are about to drop the column `lala` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "lala";

-- CreateTable
CREATE TABLE "UserLocker" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"clock" INTEGER NOT NULL,
"ciphertext" TEXT NOT NULL,
"nonce" TEXT NOT NULL,
"commitment" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "UserLocker_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "UserLocker_userId_clock_key" ON "UserLocker"("userId", "clock");

-- AddForeignKey
ALTER TABLE "UserLocker" ADD CONSTRAINT "UserLocker_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
15 changes: 14 additions & 1 deletion apps/server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,20 @@ model User {
loginAttempt LoginAttempt?
createdAt DateTime @default(now())
documents UsersOnDocuments[]
lala String?
userLocker UserLocker[]
}

model UserLocker {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
clock Int
ciphertext String
nonce String
commitment String
createdAt DateTime @default(now())
@@unique([userId, clock])
}

model Session {
Expand Down
53 changes: 53 additions & 0 deletions apps/server/src/db/createUserLocker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Prisma } from "@prisma/client";
import { prisma } from "./prisma.js";

type Params = {
userId: string;
ciphertext: string;
nonce: string;
commitment: string;
clock: number;
};

export const createUserLocker = async ({
userId,
ciphertext,
clock,
commitment,
nonce,
}: Params) => {
return await prisma.$transaction(
async (prisma) => {
const userLocker = await prisma.userLocker.findFirst({
where: {
userId,
},
orderBy: {
clock: "desc",
},
});

console.log("userLocker", userLocker, clock);

if (userLocker === null) {
if (clock !== 0) {
throw new Error("Invalid clock, expected 0");
}
} else if (userLocker.clock + 1 !== clock) {
throw new Error("Invalid clock");
}

const locker = await prisma.userLocker.create({
data: {
userId,
ciphertext,
nonce,
commitment,
clock,
},
});
return { id: locker.id };
},
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable }
);
};
13 changes: 13 additions & 0 deletions apps/server/src/db/getLatestUserLocker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { prisma } from "./prisma.js";

export const getLatestUserLocker = async (userId: string) => {
const locker = await prisma.userLocker.findFirst({
where: {
userId,
},
orderBy: {
clock: "desc",
},
});
return locker;
};
Loading

0 comments on commit 3ae2567

Please sign in to comment.