diff options
| author | Adam Soltys <[email protected]> | 2023-01-06 12:23:40 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2023-01-06 12:23:40 -0800 |
| commit | 46e33eb5ba63276836322b11abfbff6432c98ff6 (patch) | |
| tree | 64c1ad3760ed5c6c5f86dda8a0ec624197a75ab1 /src | |
| parent | e398ccf8b21e38c5fd0bc445acce4d6542551713 (diff) | |
| download | coinos-ui-46e33eb5ba63276836322b11abfbff6432c98ff6.tar.gz coinos-ui-46e33eb5ba63276836322b11abfbff6432c98ff6.zip | |
re-encrypt entropy on password change
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/RegisterOrLogin.svelte | 2 | ||||
| -rw-r--r-- | src/lib/nostr.js | 38 | ||||
| -rw-r--r-- | src/lib/utils.js | 13 | ||||
| -rw-r--r-- | src/locales/en.json | 6 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/settings/[...tab]/+page.svelte | 86 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/settings/[...tab]/_account.svelte | 85 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/settings/[...tab]/_pos.svelte | 9 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/settings/[...tab]/_security.svelte | 29 | ||||
| -rw-r--r-- | src/routes/(app)/otpsecret/+server.js | 4 |
9 files changed, 141 insertions, 131 deletions
diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte index cc23d8ad..23a045a0 100644 --- a/src/components/RegisterOrLogin.svelte +++ b/src/components/RegisterOrLogin.svelte @@ -16,6 +16,8 @@ let code = []; let redirect; + $password = undefined; + $: need2fa = form?.message === '2fa'; $: if (need2fa) tokenInput && tick().then(tokenInput.focusFirstInput); diff --git a/src/lib/nostr.js b/src/lib/nostr.js index ae18fbf1..35d843b2 100644 --- a/src/lib/nostr.js +++ b/src/lib/nostr.js @@ -131,3 +131,41 @@ export let sign = async ({ event, user }) => { export let send = (event) => { return post('/events', { event }); }; + +export let reEncryptEntropy = async (user, newPassword) => { + let { cipher, username, salt } = user; + let password = get(pw); + + if (!password) { + passwordPrompt.set(true); + } + + try { + await post('/password', { password }); + } catch (e) { + passwordPrompt.set(true); + } + + await wait(() => !!get(pw)); + password = get(pw); + + let mnemonic, key, seed, entropy, child, privkey; + entropy = Buffer.from( + await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: new Uint8Array(16) }, + await stretch(password, Buffer.from(salt, 'hex')), + Uint8Array.from(fromWords(decode(cipher, 180).words)) + ), + 'hex' + ).toString('hex'); + + let bytes = new Uint8Array( + await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: new Uint8Array(16) }, + await stretch(newPassword, Buffer.from(salt, 'hex')), + Uint8Array.from(Buffer.from(entropy, 'hex')) + ) + ); + + return encode('en', toWords(bytes), 180); +}; diff --git a/src/lib/utils.js b/src/lib/utils.js index d7fba2fc..77ed93bd 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -46,12 +46,12 @@ export const post = (url, body, headers) => { if (r.ok) return r.text(); let text = await r.text(); - let message; + let message; try { ({ message } = JSON.parse(text)); } catch (e) {} - if (message) throw new Error(message); + if (message) throw new Error(message); throw new Error(text); }) .then((body) => { @@ -83,7 +83,14 @@ export function reverseFormat(val, locale) { return Number.isNaN(reversedVal) ? 0 : +reversedVal; } -export let protectedRoutes = [/customers/, /dashboard/, /settings/, /transactions/, /\/ln\//, /request/]; +export let protectedRoutes = [ + /customers/, + /dashboard/, + /settings/, + /transactions/, + /\/ln\//, + /request/ +]; export const success = (m) => { toast.pop(); diff --git a/src/locales/en.json b/src/locales/en.json index b4c8f8ac..a5d0ad02 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -209,16 +209,16 @@ "locale": "Language", "username": "Username", "email": "Email", - "newPassword": "New Password", + "newPassword": "Change Password", "about": "About", - "aboutPlaceholder": "A short description of you or your business (address, bio, etc.)", + "aboutPlaceholder": "A little something about you or your business address and hours", "emailNotifications": "Email Notifications", "emailNotificationsDescription": "Get notified via email when a change occurs on your account or when people send you payments.", "smsNotifications": "SMS Notifications", "smsNotificationsDescription": "Coinos will send you text notifications when this is enabled.", "theme": "Color Theme", "themeDescription": "Select from the default color themes.", - "localCurrency": "Local Currency", + "localCurrency": "Currency", "btcUnit": "Display bitcoin in:", "bitcoin": "bitcoin", "satoshis": "Satoshis", diff --git a/src/routes/(app)/[username]/settings/[...tab]/+page.svelte b/src/routes/(app)/[username]/settings/[...tab]/+page.svelte index b02ddca4..b2602625 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/+page.svelte +++ b/src/routes/(app)/[username]/settings/[...tab]/+page.svelte @@ -6,10 +6,10 @@ import { Icon, Spinner, Pin } from '$comp'; import { t } from '$lib/translations'; import { failure, post, success } from '$lib/utils'; - import { avatarUpload, bannerUpload, pin } from '$lib/store'; + import { avatarUpload, bannerUpload, password, pin } from '$lib/store'; import { upload } from '$lib/upload'; import { page } from '$app/stores'; - import { sign, send } from '$lib/nostr'; + import { sign, send, reEncryptEntropy } from '$lib/nostr'; import { invalidateAll } from '$app/navigation'; import Account from './_account.svelte'; @@ -50,50 +50,62 @@ let loading; async function handleSubmit() { - loading = true; - let data = new FormData(formElement); - try { - if ($avatarUpload) { - await upload($avatarUpload.file, $avatarUpload.type, $avatarUpload.progress, token); + loading = true; + let data = new FormData(formElement); + + if (data.get('password')) { + console.log('reencrypting', user.cipher); + data.set('cipher', await reEncryptEntropy(user, data.get('password'))); + console.log(data.get('cipher')); } - if ($bannerUpload) { - await upload($bannerUpload.file, $bannerUpload.type, $bannerUpload.progress, token); + try { + if ($avatarUpload) { + await upload($avatarUpload.file, $avatarUpload.type, $avatarUpload.progress, token); + } + + if ($bannerUpload) { + await upload($bannerUpload.file, $bannerUpload.type, $bannerUpload.progress, token); + } + } catch (e) { + console.log(e); + failure('Something went wrong'); } - } catch (e) { - console.log(e); - failure('Something went wrong'); - } - let event = { - pubkey: user.pubkey, - created_at: Math.floor(Date.now() / 1000), - kind: 0, - content: JSON.stringify({ - name: user.username, - about: user.address, - picture: `${PUBLIC_COINOS_URL}/public/${user.username}-profile.webp` - }), - tags: [] - }; + let event = { + pubkey: user.pubkey, + created_at: Math.floor(Date.now() / 1000), + kind: 0, + content: JSON.stringify({ + name: user.username, + about: user.address, + picture: `${PUBLIC_COINOS_URL}/public/${user.username}-profile.webp` + }), + tags: [] + }; - await sign({ event, user }); - await send(event); + await sign({ event, user }); + await send(event); - const response = await fetch(formElement.action, { - method: 'POST', - body: data - }); + const response = await fetch(formElement.action, { + method: 'POST', + body: data + }); - const result = deserialize(await response.text()); + const result = deserialize(await response.text()); - if (result.type === 'success') { - await invalidateAll(); - } + if (result.type === 'success') { + await invalidateAll(); + if (data.get('password')) $password = data.get('password'); + } - applyAction(result); - loading = false; + applyAction(result); + loading = false; + } catch (e) { + console.log(e); + failure('Problem submitting form'); + } } let loaded; @@ -117,7 +129,7 @@ {$t('user.settings.header')} </h1> - <div class="font-bold flex justify-between items-center border-b pb-3 text-secondary"> + <div class="font-bold flex justify-around items-center border-b pb-3 text-secondary"> {#each tabs as { name, key }} <a href={`/${user.username}/settings/${name}`}> <button class="hover:opacity-80" class:text-black={tab === name} diff --git a/src/routes/(app)/[username]/settings/[...tab]/_account.svelte b/src/routes/(app)/[username]/settings/[...tab]/_account.svelte index 0359f074..6b9fb2c6 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_account.svelte +++ b/src/routes/(app)/[username]/settings/[...tab]/_account.svelte @@ -1,22 +1,19 @@ <script> import { tick } from 'svelte'; import { colorTheme, avatarUpload, bannerUpload } from '$lib/store'; - import { Icon, Toggle, LocaleSelector } from '$comp'; + import { Icon } from '$comp'; import { t } from '$lib/translations'; import { upload } from '$lib/upload'; export let user; let selectedTheme = 1; - let revealPassword = false; - let password; let profileFile; let select = (type) => files[type].el.click(); $: profile = files.profile.src; $: banner = files.banner.src; - let togglePassword = () => (revealPassword = !revealPassword); let colorThemes = [ { theme: 1, color1: 'from-[#F5F7FA]', color2: 'to-[#C3CFE2]' }, { theme: 2, color1: 'from-[#FDFCFB]', color2: 'to-[#E2D1C3]' }, @@ -56,44 +53,12 @@ $bannerUpload = { file, type, progress }; } }; - - $: user.password = user.confirm = password; </script> -<div> - <label for="language" class="font-bold block mb-1">{$t('user.settings.locale')}</label> - <LocaleSelector style="select-styles block py-3 w-full" /> -</div> - -<!-- TODO -<div> - <label for="email" class="font-bold mb-1 block">{$t('user.settings.email')}</label> - <input type="email" name="email" bind:value={user.email} /> -</div> --> - -<!-- TODO fix breaking changes when user updates this field --> -<div> - <label for="username" class="font-bold mb-1 block">{$t('user.settings.username')}</label> - <input disabled type="text" name="username" bind:value={user.username} /> - <input type="hidden" name="username" bind:value={user.username} /> -</div> - -<div class="relative"> - <label for="password" class="block font-bold block mb-1">{$t('user.settings.newPassword')}</label> - {#if revealPassword} - <input name="password" type="text" bind:value={password} /> - {:else} - <input name="password" type="password" bind:value={password} /> - {/if} - <button - type="button" - on:click={togglePassword} - on:keydown={togglePassword} - class="absolute right-5 top-11" - > - <Icon icon={revealPassword ? 'eye' : 'eye-off'} /> - </button> -</div> +<!-- <div> --> +<!-- <label for="username" class="font-bold mb-1 block">{$t('user.settings.username')}</label> --> +<!-- <input type="text" name="username" bind:value={user.username} /> --> +<!-- </div> --> <div> <label for="address" class="font-bold mb-1 block">{$t('user.settings.about')}</label> @@ -105,27 +70,6 @@ /> </div> -<!-- TODO --> -<!-- <div> --> -<!-- <div class="flex justify-between items-center"> --> -<!-- <span class="font-bold">{$t('user.settings.emailNotifications')}</span> --> -<!-- <Toggle id="email-notify" /> --> -<!-- </div> --> -<!-- <p class="text-secondary mt-1 w-9/12"> --> -<!-- {$t('user.settings.emailNotificationsDescription')} --> -<!-- </p> --> -<!-- </div> --> -<!-- --> -<!-- <div> --> -<!-- <div class="flex justify-between items-center"> --> -<!-- <span class="font-bold">{$t('user.settings.smsNotifications')}</span> --> -<!-- <Toggle id="sms-notify" /> --> -<!-- </div> --> -<!-- <p class="text-secondary mt-1 w-9/12"> --> -<!-- {$t('user.settings.smsNotificationsDescription')} --> -<!-- </p> --> -<!-- </div> --> - <div> <span class="font-bold">{$t('user.settings.profileImage')}</span> @@ -192,7 +136,6 @@ <div class="flex justify-between items-center"> <span class="font-bold">{$t('user.settings.bannerImage')}</span> </div> - <!-- found missing translation --> {#if banner} <img @@ -219,7 +162,6 @@ /> {/if} - <!-- found missing translation --> <button type="button" class="border rounded-2xl font-bold w-24 text-center px-0 py-2 hover:opacity-80" @@ -240,20 +182,3 @@ <div class="text-red-600">Max file size 10MB</div> {/if} </div> - -<!-- TODO -<div> - <span class="font-bold mb-1 block">{$t('user.settings.theme')}</span> - <p class="text-secondary mb-1">{$t('user.settings.themeDescription')}</p> - {#each colorThemes as colors} - <button - type="button" - class="hover:opacity-80 mr-2 w-10 h-10 bg-gradient-to-r {colors.color1} {colors.color2} rounded-xl border-2 {selectedTheme === - colors.theme - ? 'border-black' - : ''}" - on:click={() => handleThemeClick(colors)} - on:keydown={() => handleThemeClick(colors)} - /> - {/each} -</div> --> diff --git a/src/routes/(app)/[username]/settings/[...tab]/_pos.svelte b/src/routes/(app)/[username]/settings/[...tab]/_pos.svelte index b4aa3181..bade7a99 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_pos.svelte +++ b/src/routes/(app)/[username]/settings/[...tab]/_pos.svelte @@ -1,14 +1,19 @@ <script> - import { Icon, Toggle } from '$comp'; + import { Icon, LocaleSelector } from '$comp'; import { t } from '$lib/translations'; import { success, failure } from '$lib/utils'; import { page } from '$app/stores'; export let user, rates; - const fiats = Object.keys(rates); + let fiats = Object.keys(rates); </script> <div> + <label for="language" class="font-bold block mb-1">{$t('user.settings.locale')}</label> + <LocaleSelector style="select-styles block py-3 w-full" /> +</div> + +<div> <label for="currency" class="font-bold block mb-1">{$t('user.settings.localCurrency')}</label> <select name="currency" class="select-styles block py-3 w-full" value={user.currency}> {#each fiats as fiat} diff --git a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte b/src/routes/(app)/[username]/settings/[...tab]/_security.svelte index 2d880d44..ef4b9fba 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte +++ b/src/routes/(app)/[username]/settings/[...tab]/_security.svelte @@ -11,6 +11,7 @@ export let user, submit; let settingPin, setting2fa, disabling2fa; + let password, revealPassword; let token = ''; let pinCode = []; let verifyCode = []; @@ -18,8 +19,10 @@ let verifying = false; let old; + let togglePassword = () => (revealPassword = !revealPassword); + let startVerifying = () => { - if (pinCode.join("").length < 6) return; + if (pinCode.join('').length < 6) return; verifying = true; }; @@ -81,7 +84,7 @@ if (user.haspin) { try { pin = null; - await tick(); + await tick(); submit.click(); } catch (e) { console.log(e); @@ -125,9 +128,27 @@ }; </script> -<div> - <input type="hidden" name="newpin" value={pin} /> +<input type="hidden" name="newpin" value={pin} /> +<input type="hidden" name="confirm" bind:value={password} /> + +<div class="relative mb-5"> + <label for="password" class="block font-bold block mb-1">{$t('user.settings.newPassword')}</label> + {#if revealPassword} + <input name="password" type="text" bind:value={password} /> + {:else} + <input name="password" type="password" bind:value={password} /> + {/if} + <button + type="button" + on:click={togglePassword} + on:keydown={togglePassword} + class="absolute right-5 top-11" + > + <Icon icon={revealPassword ? 'eye' : 'eye-off'} /> + </button> +</div> +<div> <span class="font-bold mb-1" >{verifying ? $t('user.settings.verifyPIN') : $t('user.settings.securityPIN')}</span > diff --git a/src/routes/(app)/otpsecret/+server.js b/src/routes/(app)/otpsecret/+server.js index f998fb17..31dbcec1 100644 --- a/src/routes/(app)/otpsecret/+server.js +++ b/src/routes/(app)/otpsecret/+server.js @@ -4,12 +4,12 @@ import { error } from '@sveltejs/kit'; export async function POST({ cookies, request }) { try { - let body = await request.json(); + let body = await request.json(); let { username, secret } = await post('/otpsecret', body, auth(cookies)); let uri = `otpauth://totp/coinos:${username}?secret=${secret}&period=30&digits=6&algorithm=SHA1&issuer=coinos`; return new Response(JSON.stringify({ secret, uri, qr: Qr.drawImg(uri, { size: 600 }) })); } catch (e) { - console.log(e) + console.log(e); throw new error(500, 'Problem saving secret'); } } |
