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 }; +};