diff --git a/web/src/App.tsx b/web/src/App.tsx
index e851183..6c8f751 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,14 +1,32 @@
-import { useEffect, useState, type FC } from "react";
-import { getDeviceId } from "./util/deviceId";
+import { type FC } from "react";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import { DbProvider } from "./context/db/provider";
+
+import IndexPage from "./pages/Index";
+import LoginPage from "./pages/Login";
+import RegisterPage from "./pages/Register";
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ },
+ {
+ path: "/login",
+ element: ,
+ },
+ {
+ path: "/register",
+ element: ,
+ },
+]);
const App: FC = () => {
- const [deviceId, setDeviceId] = useState("");
-
- useEffect(() => {
- getDeviceId().then((id) => setDeviceId(id));
- }, []);
-
- return
{deviceId}
;
+ return (
+
+
+
+ );
};
export default App;
diff --git a/web/src/context/db/db.ts b/web/src/context/db/db.ts
new file mode 100644
index 0000000..ae9dc95
--- /dev/null
+++ b/web/src/context/db/db.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from "react";
+
+interface DbContextValues {
+ db: IDBDatabase | null;
+ setDb: (db: IDBDatabase) => void;
+}
+
+export const DbContext = createContext({
+ db: null,
+ setDb: () => {},
+});
+
+export const useDbContext = () => useContext(DbContext);
diff --git a/web/src/context/db/provider.tsx b/web/src/context/db/provider.tsx
new file mode 100644
index 0000000..f25f499
--- /dev/null
+++ b/web/src/context/db/provider.tsx
@@ -0,0 +1,23 @@
+import { useCallback, useState, type FC, type ReactNode } from "react";
+import { DbContext } from "./db";
+
+interface IDBProvider {
+ children: ReactNode;
+}
+
+export const DbProvider: FC = ({ children }) => {
+ const [db, _setDb] = useState(null);
+
+ const setDb = useCallback((db: IDBDatabase) => _setDb(db), []);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/web/src/main.tsx b/web/src/main.tsx
index df4d220..bc353d6 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -1,26 +1,8 @@
import { createRoot } from "react-dom/client";
-import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
import "./index.css";
-import LoginPage from "./pages/Login";
-import RegisterPage from "./pages/Register";
const root = document.getElementById("root")!;
-const router = createBrowserRouter([
- {
- path: "/",
- element: ,
- },
- {
- path: "/login",
- element: ,
- },
- {
- path: "/register",
- element: ,
- },
-]);
-
-createRoot(root).render();
+createRoot(root).render();
diff --git a/web/src/pages/Index/index.tsx b/web/src/pages/Index/index.tsx
new file mode 100644
index 0000000..00e25ff
--- /dev/null
+++ b/web/src/pages/Index/index.tsx
@@ -0,0 +1,30 @@
+import { deriveDeviceKey, getDeviceId } from "@/util/deviceId";
+import { useEffect, useState, type FC } from "react";
+
+const IndexPage: FC = () => {
+ const [deviceId, setDeviceId] = useState("");
+ const [deviceKey, setDeviceKey] = useState("");
+
+ useEffect(() => {
+ getDeviceId().then((id) => {
+ setDeviceId(id);
+ deriveDeviceKey(id).then((key) => {
+ crypto.subtle.exportKey("raw", key).then((key) => {
+ const enc = new TextDecoder();
+ setDeviceKey(enc.decode(key));
+ });
+ });
+ });
+ }, []);
+
+ return (
+
+
Id:
+
{deviceId}
+
Key:
+
{deviceKey}
+
+ );
+};
+
+export default IndexPage;
diff --git a/web/src/util/account.ts b/web/src/util/account.ts
new file mode 100644
index 0000000..8f8ddc3
--- /dev/null
+++ b/web/src/util/account.ts
@@ -0,0 +1,30 @@
+import { deriveDeviceKey, getDeviceId } from "./deviceId";
+import { encryptToken } from "./token";
+
+const storeTokensForAccount = async (
+ accountId: string,
+ accessToken: string,
+ refreshToken: string
+) => {
+ const deviceKeyId = await getDeviceId();
+ const key = await deriveDeviceKey(deviceKeyId);
+
+ const access = await encryptToken(accessToken, key);
+ const refresh = await encryptToken(refreshToken, key);
+
+ const entry = {
+ accountId,
+ label: `Account for ${accountId}`,
+ access: {
+ data: Array.from(new Uint8Array(access.cipherText)),
+ iv: Array.from(access.iv),
+ },
+ refresh: {
+ data: Array.from(new Uint8Array(refresh.cipherText)),
+ iv: Array.from(refresh.iv),
+ },
+ updatedAt: new Date().toISOString(),
+ };
+
+ // Save this `entry` in IndexedDB (or use a localforage wrapper)
+};
diff --git a/web/src/util/deviceId.ts b/web/src/util/deviceId.ts
index 8aa4134..9ac9233 100644
--- a/web/src/util/deviceId.ts
+++ b/web/src/util/deviceId.ts
@@ -29,3 +29,26 @@ export const getDeviceId = async () => {
return deviceId; // A 64-character hex string
};
+
+export const deriveDeviceKey = async (deviceKeyId: string) => {
+ const encoder = new TextEncoder();
+ const baseKey = await crypto.subtle.importKey(
+ "raw",
+ encoder.encode(deviceKeyId),
+ { name: "PBKDF2" },
+ false,
+ ["deriveKey"]
+ );
+ return crypto.subtle.deriveKey(
+ {
+ name: "PBKDF2",
+ salt: encoder.encode("guard_salt"),
+ iterations: 100000,
+ hash: "SHA-256",
+ },
+ baseKey,
+ { name: "AES-GCM", length: 256 },
+ false,
+ ["encrypt", "decrypt"]
+ );
+};
diff --git a/web/src/util/token.ts b/web/src/util/token.ts
new file mode 100644
index 0000000..4ad9b29
--- /dev/null
+++ b/web/src/util/token.ts
@@ -0,0 +1,18 @@
+export type EncryptedToken = {
+ cipherText: ArrayBuffer;
+ iv: Uint8Array;
+};
+
+export const encryptToken = async (
+ token: string,
+ key: CryptoKey
+): Promise => {
+ const encoder = new TextEncoder();
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const cipherText = await crypto.subtle.encrypt(
+ { name: "AES-GCM", iv },
+ key,
+ encoder.encode(token)
+ );
+ return { cipherText, iv };
+};