diff --git a/materialious/src/hooks.server.ts b/materialious/src/hooks.server.ts index 71a50790..c3c0d967 100644 --- a/materialious/src/hooks.server.ts +++ b/materialious/src/hooks.server.ts @@ -1,5 +1,5 @@ -import { isOwnBackend } from '$lib/backend'; -import { sequelize } from '$lib/backendOnly/database'; +import { isOwnBackend } from '$lib/shared'; +import { sequelize } from '$lib/server/database'; import { unsign } from 'cookie-signature'; import { env } from '$env/dynamic/private'; @@ -10,6 +10,7 @@ export async function handle({ event, resolve }) { } if (!sequelizeAuthenticated) { + await sequelize.sync(); await sequelize.authenticate(); sequelizeAuthenticated = true; } diff --git a/materialious/src/lib/backend.ts b/materialious/src/lib/backend.ts index ec96cad3..bda8e0f0 100644 --- a/materialious/src/lib/backend.ts +++ b/materialious/src/lib/backend.ts @@ -1,28 +1,55 @@ import sodium from 'libsodium-wrappers-sumo'; +import { rawSubscriptionKeyStore } from './store'; -export type IsOwnBackend = { - builtWithBackend: boolean; - internalAuth: boolean; - requireAuth: boolean; - registrationAllowed: boolean; -}; +export async function createUserBackend(username: string, rawPassword: string): Promise { + await sodium.ready; -export function isOwnBackend(): IsOwnBackend | null { - if (import.meta.env.VITE_BUILD_WITH_BACKEND !== 'true') return null; + const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES); - return { - builtWithBackend: true, - internalAuth: import.meta.env.VITE_INTERNAL_AUTH !== 'false', - requireAuth: import.meta.env.VITE_REQUIRE_AUTH !== 'false', - registrationAllowed: import.meta.env.VITE_REGISTRATION_ALLOWED === 'true' - }; + const loginHash = sodium.crypto_pwhash( + 32, + rawPassword, + passwordSalt, + sodium.crypto_pwhash_OPSLIMIT_SENSITIVE, + sodium.crypto_pwhash_MEMLIMIT_SENSITIVE, + sodium.crypto_pwhash_ALG_DEFAULT + ); + + const subscriptionPasswordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES); + + const userCreateResp = await fetch('/api/user/create', { + method: 'POST', + body: JSON.stringify({ + username: username, + password: { + hash: sodium.to_base64(loginHash), + salt: sodium.to_base64(passwordSalt) + }, + subscriptionPasswordSalt: sodium.to_base64(subscriptionPasswordSalt) + }) + }); + + if (!userCreateResp.ok) return false; + + const subscriptionRawKey = sodium.crypto_pwhash( + 32, + rawPassword, + passwordSalt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT + ); + + rawSubscriptionKeyStore.set(sodium.to_base64(subscriptionRawKey)); + + return true; } -export async function backendLogin(username: string, rawPassword: string) { +export async function loginUserBackend(username: string, rawPassword: string): Promise { await sodium.ready; const passwordSaltsResp = await fetch(`/api/user/${username}/public`); - if (!passwordSaltsResp.ok) return; + if (!passwordSaltsResp.ok) return false; const passwordSalts = await passwordSaltsResp.json(); @@ -39,11 +66,11 @@ export async function backendLogin(username: string, rawPassword: string) { method: 'POST', body: JSON.stringify({ username, - passwordHash: loginHash + passwordHash: sodium.to_base64(loginHash) }) }); - if (!loginResp.ok) return; + if (!loginResp.ok) return false; const subscriptionRawKey = sodium.crypto_pwhash( 32, @@ -53,4 +80,8 @@ export async function backendLogin(username: string, rawPassword: string) { sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, sodium.crypto_pwhash_ALG_DEFAULT ); + + rawSubscriptionKeyStore.set(sodium.to_base64(subscriptionRawKey)); + + return true; } diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 92dea445..6a999fc9 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -28,7 +28,7 @@ import { page } from '$app/state'; import { Capacitor } from '@capacitor/core'; import { Share } from '@capacitor/share'; import { Clipboard } from '@capacitor/clipboard'; -import { isOwnBackend } from './backend'; +import { isOwnBackend } from './shared'; export function isMobile(): boolean { const userAgent = navigator.userAgent; diff --git a/materialious/src/lib/backendOnly/database.ts b/materialious/src/lib/server/database.ts similarity index 100% rename from materialious/src/lib/backendOnly/database.ts rename to materialious/src/lib/server/database.ts diff --git a/materialious/src/lib/backendOnly/user.ts b/materialious/src/lib/server/user.ts similarity index 93% rename from materialious/src/lib/backendOnly/user.ts rename to materialious/src/lib/server/user.ts index c0ae586d..fdbd8fee 100644 --- a/materialious/src/lib/backendOnly/user.ts +++ b/materialious/src/lib/server/user.ts @@ -67,7 +67,17 @@ export async function createUser(user: CreateUser): Promise { subscriptionPasswordSalt: user.subscriptionPasswordSalt }; - await UserTable.create(createdUser); + let userCreated = false; + try { + await UserTable.create(createdUser); + userCreated = true; + } catch { + userCreated = false; + } + + if (!userCreated) { + throw error(400); + } return new User(createdUser as UserTableModel); } diff --git a/materialious/src/lib/shared/index.ts b/materialious/src/lib/shared/index.ts new file mode 100644 index 00000000..99eb30d8 --- /dev/null +++ b/materialious/src/lib/shared/index.ts @@ -0,0 +1,17 @@ +export type IsOwnBackend = { + builtWithBackend: boolean; + internalAuth: boolean; + requireAuth: boolean; + registrationAllowed: boolean; +}; + +export function isOwnBackend(): IsOwnBackend | null { + if (import.meta.env.VITE_BUILD_WITH_BACKEND !== 'true') return null; + + return { + builtWithBackend: true, + internalAuth: import.meta.env.VITE_INTERNAL_AUTH !== 'false', + requireAuth: import.meta.env.VITE_REQUIRE_AUTH !== 'false', + registrationAllowed: import.meta.env.VITE_REGISTRATION_ALLOWED === 'true' + }; +} diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index 85160e65..3c4b7e66 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -333,6 +333,12 @@ export const engineFallbacksStore: Writable = persist( 'engineFallbacks' ); +export const rawSubscriptionKeyStore: Writable = persist( + writable(), + createStorage(), + 'rawSubscriptionKey' +); + export const syncPartyPeerStore: Writable = writable(null); export const syncPartyConnectionsStore: Writable = writable(); diff --git a/materialious/src/routes/(app)/+layout.svelte b/materialious/src/routes/(app)/+layout.svelte index bba76f3e..99108234 100644 --- a/materialious/src/routes/(app)/+layout.svelte +++ b/materialious/src/routes/(app)/+layout.svelte @@ -42,7 +42,8 @@ import { isYTBackend, clearCaches, truncate } from '$lib/misc'; import Author from '$lib/components/Author.svelte'; import Toast from '$lib/components/Toast.svelte'; - import { isOwnBackend } from '$lib/backend'; + import { createUserBackend } from '$lib/backend'; + import { isOwnBackend } from '$lib/shared'; let { children } = $props(); @@ -116,6 +117,7 @@ async function login() { if (isOwnBackend()?.internalAuth) { + await createUserBackend('ward', 'password123'); return; } diff --git a/materialious/src/routes/+layout.ts b/materialious/src/routes/+layout.ts index aca23844..609e9d8f 100644 --- a/materialious/src/routes/+layout.ts +++ b/materialious/src/routes/+layout.ts @@ -11,7 +11,8 @@ import { backendInUseStore, instanceStore, interfaceDefaultPage, - isAndroidTvStore + isAndroidTvStore, + rawSubscriptionKeyStore } from '$lib/store'; import { get, type Writable } from 'svelte/store'; import { Capacitor } from '@capacitor/core'; @@ -28,13 +29,14 @@ export async function load({ url }) { isAndroidTvStore.set((await androidTv.isAndroidTv()).value); - // Due to race condition with how we set & save persistent store values - // we manually set stores like auth & instance before load. if (Capacitor.getPlatform() === 'android') { + // Due to race condition with how we set & save persistent store values + // we manually set stores like auth & instance before load. const preferenceKey: Record> = { invidiousInstance: instanceStore, authToken: authStore, - backendInUse: backendInUseStore + backendInUse: backendInUseStore, + rawSubscriptionKey: rawSubscriptionKeyStore }; for (const [key, store] of Object.entries(preferenceKey)) { diff --git a/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts b/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts index a8d8dfc7..68c198a1 100644 --- a/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts +++ b/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts @@ -1,4 +1,4 @@ -import { isOwnBackend } from '$lib/backend'; +import { isOwnBackend } from '$lib/shared'; import psl from 'psl'; import { env } from '$env/dynamic/private'; diff --git a/materialious/src/routes/api/user/[userId]/public/+server.ts b/materialious/src/routes/api/user/[userId]/public/+server.ts index f8287bcf..605c4f64 100644 --- a/materialious/src/routes/api/user/[userId]/public/+server.ts +++ b/materialious/src/routes/api/user/[userId]/public/+server.ts @@ -1,7 +1,12 @@ -import { getUser } from '$lib/backendOnly/user'; +import { getUser } from '$lib/server/user'; import { json } from '@sveltejs/kit'; +import { isOwnBackend } from '$lib/shared'; export async function GET({ params }) { + if (!isOwnBackend()?.internalAuth) { + return new Response('', { status: 500 }); + } + const user = await getUser(params.userId); return json(user.publicPasswordSalts); } diff --git a/materialious/src/routes/api/user/create/+server.ts b/materialious/src/routes/api/user/create/+server.ts new file mode 100644 index 00000000..93ae86bd --- /dev/null +++ b/materialious/src/routes/api/user/create/+server.ts @@ -0,0 +1,34 @@ +import { isOwnBackend } from '$lib/shared'; +import { createUser } from '$lib/server/user.js'; +import { error } from '@sveltejs/kit'; +import z from 'zod'; + +const zUserCreate = z.object({ + username: z.string().min(3).max(32), + password: z.object({ + hash: z.string().max(255), + salt: z.string().max(255) + }), + subscriptionPasswordSalt: z.string().max(255) +}); + +export async function POST({ request }) { + if (!isOwnBackend()?.internalAuth || !isOwnBackend()?.registrationAllowed) { + return new Response('', { status: 500 }); + } + + const userToCreate = zUserCreate.safeParse(await request.json()); + + if (!userToCreate.success) throw error(400); + + await createUser({ + username: userToCreate.data.username, + password: { + hash: userToCreate.data.password.hash, + salt: userToCreate.data.password.salt + }, + subscriptionPasswordSalt: userToCreate.data.subscriptionPasswordSalt + }); + + return new Response(''); +} diff --git a/materialious/src/routes/api/user/login/+server.ts b/materialious/src/routes/api/user/login/+server.ts index 67d62e8b..24aad346 100644 --- a/materialious/src/routes/api/user/login/+server.ts +++ b/materialious/src/routes/api/user/login/+server.ts @@ -1,8 +1,9 @@ -import { authenticateUser, User } from '$lib/backendOnly/user'; +import { authenticateUser } from '$lib/server/user'; import { error } from '@sveltejs/kit'; import z from 'zod'; import { env } from '$env/dynamic/private'; import { sign } from 'cookie-signature'; +import { isOwnBackend } from '$lib/shared'; const zUserLogin = z.object({ username: z.string(), @@ -10,6 +11,10 @@ const zUserLogin = z.object({ }); export async function POST({ request, cookies }) { + if (!isOwnBackend()?.internalAuth) { + return new Response('', { status: 500 }); + } + const userLogin = zUserLogin.safeParse(await request.json()); if (!userLogin.success) throw error(401);