Derive login hash in background
This commit is contained in:
Generated
+7
@@ -27,6 +27,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"capacitor-music-controls-plugin": "^6.1.0",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"comlink": "^4.4.2",
|
||||
"cookie-signature": "^1.2.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"dexie": "^4.2.1",
|
||||
@@ -6340,6 +6341,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/comlink": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
|
||||
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"capacitor-music-controls-plugin": "^6.1.0",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"comlink": "^4.4.2",
|
||||
"cookie-signature": "^1.2.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"dexie": "^4.2.1",
|
||||
|
||||
@@ -111,22 +111,19 @@ export async function postSubscribeBackend(authorId: string) {
|
||||
if (resp.ok) parseChannelRSS(authorId);
|
||||
}
|
||||
|
||||
export type DerivePassword = (rawPassword: string, passwordSalt: Uint8Array) => Promise<Uint8Array>;
|
||||
|
||||
export async function createUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const loginHash = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
passwordSalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const loginHash = await derivePassword(rawPassword, passwordSalt);
|
||||
|
||||
const decryptionKeySalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
@@ -175,7 +172,8 @@ export async function createUserBackend(
|
||||
export async function loginUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
@@ -184,13 +182,9 @@ export async function loginUserBackend(
|
||||
|
||||
const passwordSalts = await passwordSaltsResp.json();
|
||||
|
||||
const loginHash = sodium.crypto_pwhash(
|
||||
32,
|
||||
const loginHash = await derivePassword(
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.passwordSalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
sodium.from_base64(passwordSalts.passwordSalt)
|
||||
);
|
||||
|
||||
const loginResp = await fetch('/api/user/login', {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { createUserBackend, loginUserBackend } from '$lib/api/backend';
|
||||
import { createUserBackend, loginUserBackend, type DerivePassword } from '$lib/api/backend';
|
||||
import PageLoading from '$lib/components/PageLoading.svelte';
|
||||
import { _ } from '$lib/i18n';
|
||||
import 'altcha';
|
||||
import * as comlink from 'comlink';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let needToRegister = $state(false);
|
||||
|
||||
@@ -11,88 +14,110 @@
|
||||
let rawPassword = $state('');
|
||||
let captchaPayload = $state('');
|
||||
|
||||
let worker: Worker | undefined;
|
||||
let derivePassword: DerivePassword;
|
||||
|
||||
let isLoading = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
worker = new Worker(new URL('./workers/derivePassword.ts', import.meta.url), {
|
||||
type: 'module'
|
||||
});
|
||||
const workerApi = comlink.wrap(worker);
|
||||
|
||||
derivePassword = (workerApi as any).derivePassword as DerivePassword;
|
||||
});
|
||||
|
||||
let failed = $state(false);
|
||||
async function onLogin(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
isLoading = true;
|
||||
|
||||
if (needToRegister) {
|
||||
failed = !(await createUserBackend(username, rawPassword, captchaPayload));
|
||||
failed = !(await createUserBackend(username, rawPassword, captchaPayload, derivePassword));
|
||||
} else {
|
||||
failed = !(await loginUserBackend(username, rawPassword, captchaPayload));
|
||||
failed = !(await loginUserBackend(username, rawPassword, captchaPayload, derivePassword));
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
|
||||
if (!failed) {
|
||||
goto(resolve('/', {}), { replaceState: true });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="center-align">
|
||||
<article class="padding left-align">
|
||||
<h3>{$_(needToRegister ? 'createAccount' : 'login')}</h3>
|
||||
<p class="no-margin">{$_(needToRegister ? 'materialiousCreate' : 'materialiousLogin')}</p>
|
||||
{#if isLoading}
|
||||
<PageLoading />
|
||||
{:else}
|
||||
<nav class="center-align">
|
||||
<article class="padding left-align">
|
||||
<h3>{$_(needToRegister ? 'createAccount' : 'login')}</h3>
|
||||
<p class="no-margin">{$_(needToRegister ? 'materialiousCreate' : 'materialiousLogin')}</p>
|
||||
|
||||
<form onsubmit={onLogin}>
|
||||
<div
|
||||
class="field label prefix surface-container-highest"
|
||||
class:invalid={failed && needToRegister}
|
||||
>
|
||||
<i>person</i>
|
||||
<input bind:value={username} name="username" type="text" />
|
||||
<label for="username">{$_('username')}</label>
|
||||
{#if failed && needToRegister}
|
||||
<output class="invalid">{$_('usernameTaken')}</output>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="field label prefix surface-container-highest"
|
||||
class:invalid={failed && !needToRegister}
|
||||
>
|
||||
<i>password</i>
|
||||
<input bind:value={rawPassword} name="password" type="password" />
|
||||
<label for="password">{$_('password')}</label>
|
||||
{#if failed && !needToRegister}
|
||||
<output class="invalid">{$_('invalidPassword')}</output>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<article
|
||||
class="surface-container-highest no-padding"
|
||||
style="width: 100%;height: fit-content;"
|
||||
>
|
||||
<altcha-widget
|
||||
challengeurl="/api/captcha"
|
||||
hidelogo
|
||||
hidefooter
|
||||
onstatechange={(ev) => {
|
||||
const { payload, state } = ev.detail;
|
||||
if (state === 'verified' && payload) {
|
||||
captchaPayload = payload;
|
||||
}
|
||||
}}
|
||||
></altcha-widget>
|
||||
</article>
|
||||
|
||||
<nav class="right-align">
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
onclick={() => {
|
||||
needToRegister = !needToRegister;
|
||||
failed = false;
|
||||
}}
|
||||
<form onsubmit={onLogin}>
|
||||
<div
|
||||
class="field label prefix surface-container-highest"
|
||||
class:invalid={failed && needToRegister}
|
||||
>
|
||||
<span>{$_(!needToRegister ? 'needRegister' : 'needLogin')}</span>
|
||||
</button>
|
||||
<i>person</i>
|
||||
<input bind:value={username} name="username" type="text" />
|
||||
<label for="username">{$_('username')}</label>
|
||||
{#if failed && needToRegister}
|
||||
<output class="invalid">{$_('usernameTaken')}</output>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="field label prefix surface-container-highest"
|
||||
class:invalid={failed && !needToRegister}
|
||||
>
|
||||
<i>password</i>
|
||||
<input bind:value={rawPassword} name="password" type="password" />
|
||||
<label for="password">{$_('password')}</label>
|
||||
{#if failed && !needToRegister}
|
||||
<output class="invalid">{$_('invalidPassword')}</output>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button type="submit">
|
||||
<i>done</i>
|
||||
<span>{$_(needToRegister ? 'createAccount' : 'login')}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</form>
|
||||
</article>
|
||||
</nav>
|
||||
<article
|
||||
class="surface-container-highest no-padding"
|
||||
style="width: 100%;height: fit-content;"
|
||||
>
|
||||
<altcha-widget
|
||||
challengeurl="/api/captcha"
|
||||
hidelogo
|
||||
hidefooter
|
||||
onstatechange={(ev) => {
|
||||
const { payload, state } = ev.detail;
|
||||
if (state === 'verified' && payload) {
|
||||
captchaPayload = payload;
|
||||
}
|
||||
}}
|
||||
></altcha-widget>
|
||||
</article>
|
||||
|
||||
<nav class="right-align">
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
onclick={() => {
|
||||
needToRegister = !needToRegister;
|
||||
failed = false;
|
||||
}}
|
||||
>
|
||||
<span>{$_(!needToRegister ? 'needRegister' : 'needLogin')}</span>
|
||||
</button>
|
||||
|
||||
<button type="submit">
|
||||
<i>done</i>
|
||||
<span>{$_(needToRegister ? 'createAccount' : 'login')}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</form>
|
||||
</article>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
article {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as comlink from 'comlink';
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
|
||||
async function derivePassword(rawPassword: string, passwordSalt: Uint8Array): Promise<Uint8Array> {
|
||||
await sodium.ready;
|
||||
|
||||
return sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
passwordSalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
}
|
||||
|
||||
comlink.expose({ derivePassword });
|
||||
Reference in New Issue
Block a user