passphrase prompt

This commit is contained in:
2023-05-23 00:44:51 +02:00
parent 6a53d9fd58
commit 6f19ee94d1
6 changed files with 110 additions and 27 deletions

1
components.d.ts vendored
View File

@@ -23,6 +23,7 @@ declare module '@vue/runtime-core' {
SearchBar: typeof import('./src/components/Search/SearchBar.vue')['default'] SearchBar: typeof import('./src/components/Search/SearchBar.vue')['default']
SearchResult: typeof import('./src/components/Search/SearchResult.vue')['default'] SearchResult: typeof import('./src/components/Search/SearchResult.vue')['default']
SideBar: typeof import('./src/components/SideBar.vue')['default'] SideBar: typeof import('./src/components/SideBar.vue')['default']
SideBar2: typeof import('./src/components/SideBar2.vue')['default']
SideBarMenu: typeof import('./src/components/SideBar/SideBarMenu.vue')['default'] SideBarMenu: typeof import('./src/components/SideBar/SideBarMenu.vue')['default']
SideBarMenuItem: typeof import('./src/components/SideBar/SideBarMenuItem.vue')['default'] SideBarMenuItem: typeof import('./src/components/SideBar/SideBarMenuItem.vue')['default']
SkeletonNote: typeof import('./src/components/Skeleton/SkeletonNote.vue')['default'] SkeletonNote: typeof import('./src/components/Skeleton/SkeletonNote.vue')['default']

View File

@@ -1,6 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { activeNote, updateNote, notes } from '@/composables/useNotes' import { activeNote, updateNote, notes, activeNotesSource } from '@/composables/useNotes'
import { viewModes, activeViewMode } from '@/composables/useViewMode' import { viewModes, activeViewMode } from '@/composables/useViewMode'
import {
getClientKey,
getEncryptionKey,
setClientKey,
passphraseRequired
} from '@/composables/useEncryption'
import { windowIsMobile } from '@/utils/helpers' import { windowIsMobile } from '@/utils/helpers'
import firebase from 'firebase/compat/app' import firebase from 'firebase/compat/app'
import * as firebaseui from 'firebaseui' import * as firebaseui from 'firebaseui'
@@ -15,7 +21,29 @@ const firebaseAuthUI =
firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(firebase.auth()) firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(firebase.auth())
provide('firebaseAuthUI', firebaseAuthUI) provide('firebaseAuthUI', firebaseAuthUI)
const loading = computed(() => notes.value.length === 0) watch(
activeNotesSource,
() => {
if (activeNotesSource.value === 'firebase') {
getClientKey()
getEncryptionKey()
}
},
{ immediate: true }
)
const passphrase = ref('')
const submitPassphrase = (close: () => void) => {
const passphraseValid = setClientKey(passphrase.value)
if (!passphraseValid) {
console.log('passphrase is invalid')
} else {
close()
}
}
const loading = computed(() => notes.value.length === 0 || passphraseRequired.value)
provide('loading', loading) provide('loading', loading)
</script> </script>
@@ -31,9 +59,10 @@ provide('loading', loading)
@set-view-mode="(viewMode) => (activeViewMode = viewMode)" @set-view-mode="(viewMode) => (activeViewMode = viewMode)"
@collapse="(collapse) => (sideBarCollapsed = collapse)" @collapse="(collapse) => (sideBarCollapsed = collapse)"
class="mt-[50px] px-3 py-6" class="mt-[50px] px-3 py-6"
:class="sideBarCollapsed && 'max-sm:hidden'"
/> />
<main <main
class="transition[margin-left] z-10 mt-[50px] w-full border-x-[1px] bg-white px-10 py-6 duration-200 ease-out" class="transition[margin-left] z-10 mt-[50px] w-full border-x-[1px] bg-white px-10 py-6 duration-200 ease-out max-sm:px-4 max-sm:py-2"
:class="sideBarCollapsed ? 'ml-0' : 'ml-sidebar max-sm:hidden'" :class="sideBarCollapsed ? 'ml-0' : 'ml-sidebar max-sm:hidden'"
> >
<template v-if="!loading"> <template v-if="!loading">
@@ -50,4 +79,15 @@ provide('loading', loading)
<SkeletonNote v-else /> <SkeletonNote v-else />
</main> </main>
</div> </div>
<Modal :open="passphraseRequired" :persistent="true">
<template #default>
<p>
Your notes are encrypted. Please enter your encryption key passphrase to decrypt your notes.
</p>
<input type="password" class="input-bordered input mt-4 w-full" v-model="passphrase" />
</template>
<template #actions="{ close }">
<button class="btn-primary btn-sm btn" @click="submitPassphrase(close)">Submit</button>
</template>
</Modal>
</template> </template>

View File

@@ -4,13 +4,20 @@ import { onClickOutside } from '@vueuse/core'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
open?: boolean open?: boolean
persistent?: boolean
}>(), }>(),
{ {
open: false open: false,
persistent: false
} }
) )
const show = ref<boolean>(Boolean(props.open)) const show = ref<boolean>(false)
watch(
() => props.open,
() => (show.value = props.open),
{ immediate: true }
)
const modal = ref<HTMLElement | null>(null) const modal = ref<HTMLElement | null>(null)
const modalBox = ref(null) const modalBox = ref(null)
@@ -25,7 +32,7 @@ const close = (): Promise<boolean> => {
const slotProps = { open, close } const slotProps = { open, close }
onClickOutside(modalBox, () => close()) if (!props.persistent) onClickOutside(modalBox, () => close())
const onEnter = (el: Element, done: () => void): void => { const onEnter = (el: Element, done: () => void): void => {
setTimeout(() => { setTimeout(() => {

View File

@@ -2,7 +2,7 @@ import { doc, getDoc } from 'firebase/firestore'
import { user, db } from '@/composables/useFirebase' import { user, db } from '@/composables/useFirebase'
import { decrypt, calculateClientKey } from '@/utils/crypto' import { decrypt, calculateClientKey } from '@/utils/crypto'
function getClientKeysFromLocalStorage() { function getClientKeysFromLocalStorage(): { [uid: string]: string } {
try { try {
return JSON.parse(localStorage.getItem('clientKeys') || '{}') return JSON.parse(localStorage.getItem('clientKeys') || '{}')
} catch (e) { } catch (e) {
@@ -10,24 +10,58 @@ function getClientKeysFromLocalStorage() {
} }
} }
export const getClientKey = (): ClientKey | void => { export const clientKey = ref<ClientKey>()
export const getClientKey = () => {
if (!user.value) return if (!user.value) return
const clientKeys = getClientKeysFromLocalStorage() const clientKeys = getClientKeysFromLocalStorage()
const clientKey = clientKeys[user.value?.uid] || calculateClientKey('test') clientKey.value = clientKeys[user.value?.uid]
return clientKey
} }
export async function getEncryptionKey(): Promise<EncryptionKey | void> { export const setClientKey = (passphrase: string) => {
if (!user.value) return const calculatedClientKey = calculateClientKey(passphrase)
const clientKey = getClientKey() const verified = verifyClientKey(calculatedClientKey)
if (!db.value || !clientKey) return if (!user.value || !verified) return
const data = (await getDoc(doc(db.value, 'encryptionKeys', user.value?.uid || ''))).data() const clientKeys = getClientKeysFromLocalStorage()
if (!data) return clientKeys[user.value.uid] = calculatedClientKey
const { key } = data localStorage.setItem('clientKeys', JSON.stringify(clientKeys))
const encryptionKey: EncryptionKey = decrypt(key, clientKey) clientKey.value = calculatedClientKey
return encryptionKey getEncryptionKey()
return true
} }
export const verifyClientKey = (clientKey: ClientKey) => {
try {
if (!encryptedEncryptionKey.value) throw new Error('Encryption key is null')
if (!clientKey) throw new Error('Client key is null')
decrypt(encryptedEncryptionKey.value, clientKey)
return true
} catch (e) {
console.log(e)
return false
}
}
const encryptedEncryptionKey = ref<EncryptedEncryptionKey>()
async function getEncryptedEncryptionKey(): Promise<EncryptedEncryptionKey | void> {
if (!user.value || !db.value) return
const data = (await getDoc(doc(db.value, 'encryptionKeys', user.value?.uid || ''))).data()
return data?.key
}
export const encryptionKey = ref<EncryptionKey | null>()
export async function getEncryptionKey() {
encryptedEncryptionKey.value = (await getEncryptedEncryptionKey()) || undefined
if (!encryptedEncryptionKey.value || !clientKey.value) return
encryptionKey.value = decrypt(encryptedEncryptionKey.value, clientKey.value)
}
export const passphraseRequired = computed(() => {
return Boolean(encryptedEncryptionKey.value && !clientKey.value)
})
const decryptNote = (note: BaseNote, key: EncryptionKey) => { const decryptNote = (note: BaseNote, key: EncryptionKey) => {
return { return {
...note, ...note,

View File

@@ -3,19 +3,19 @@ import { useTitle } from '@vueuse/core'
import { doc, getDoc } from 'firebase/firestore' import { doc, getDoc } from 'firebase/firestore'
import { viewModes, activeViewMode } from '@/composables/useViewMode' import { viewModes, activeViewMode } from '@/composables/useViewMode'
import { initialized, user, db } from '@/composables/useFirebase' import { initialized, user, db } from '@/composables/useFirebase'
import { decryptNotes, getEncryptionKey } from '@/composables/useEncryption' import { decryptNotes, encryptionKey } from '@/composables/useEncryption'
import { defaultNotes } from '@/utils/defaultNotes' import { defaultNotes } from '@/utils/defaultNotes'
import { mdToHtml } from '@/utils/markdown' import { mdToHtml } from '@/utils/markdown'
import { getAllMatches } from '@/utils/helpers' import { getAllMatches } from '@/utils/helpers'
const notesSources = computed(() => ({ export const notesSources = computed(() => ({
local: true, local: true,
firebase: initialized.value && user.value firebase: initialized.value && user.value
})) }))
type notesSourceValues = keyof typeof notesSources.value | null type notesSourceValues = keyof typeof notesSources.value | null
const activeNotesSource = ref<notesSourceValues>(null) export const activeNotesSource = ref<notesSourceValues>(null)
watchEffect(() => { watchEffect(() => {
const getSource = (): notesSourceValues => { const getSource = (): notesSourceValues => {
@@ -29,14 +29,14 @@ const baseNotes = ref<{ [noteId: string]: BaseNote }>({})
watch( watch(
baseNotes, baseNotes,
() => { () => {
console.log(`Sync base notes with ${activeNotesSource.value}`, baseNotes.value) if (!activeNotesSource.value || Object.keys(baseNotes.value).length === 0) return
if (!activeNotesSource.value) return
if (activeNotesSource.value === 'local') { if (activeNotesSource.value === 'local') {
console.log('sync with local') console.log('sync with local')
localStorage.setItem('notes', JSON.stringify(baseNotes.value)) localStorage.setItem('notes', JSON.stringify(baseNotes.value))
} else if (activeNotesSource.value === 'firebase') { } else if (activeNotesSource.value === 'firebase') {
console.log('sync with firebase') console.log('sync with firebase')
} }
console.log(`Sync base notes with ${activeNotesSource.value}`, baseNotes.value)
}, },
{ deep: true } { deep: true }
) )
@@ -209,7 +209,7 @@ const parseBaseNotes = (notes: BaseNotes): BaseNotes => {
} }
watch( watch(
activeNotesSource, [activeNotesSource, encryptionKey],
async () => { async () => {
if (!activeNotesSource.value) return if (!activeNotesSource.value) return
baseNotes.value = {} baseNotes.value = {}
@@ -221,11 +221,11 @@ watch(
console.log(error) console.log(error)
} }
} else if (activeNotesSource.value === 'firebase' && db.value) { } else if (activeNotesSource.value === 'firebase' && db.value) {
if (encryptionKey.value === undefined) return
const firebaseNotes = ( const firebaseNotes = (
await getDoc(doc(db.value, 'pages', user.value?.uid || '')) await getDoc(doc(db.value, 'pages', user.value?.uid || ''))
).data() as { [noteId: string]: any } ).data() as { [noteId: string]: any }
const encryptionKey = await getEncryptionKey() notes = encryptionKey.value ? decryptNotes(firebaseNotes, encryptionKey.value) : firebaseNotes
notes = encryptionKey ? decryptNotes(firebaseNotes, encryptionKey) : firebaseNotes
console.log('get notes from firebase', notes) console.log('get notes from firebase', notes)
} }
baseNotes.value = parseBaseNotes(notes) baseNotes.value = parseBaseNotes(notes)

1
src/types.d.ts vendored
View File

@@ -42,6 +42,7 @@ declare global {
} }
type ClientKey = string type ClientKey = string
type EncryptedEncryptionKey = string
type EncryptionKey = string type EncryptionKey = string
} }
export {} export {}