passphrase prompt
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -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']
|
||||||
|
|||||||
46
src/App.vue
46
src/App.vue
@@ -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>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
1
src/types.d.ts
vendored
@@ -42,6 +42,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClientKey = string
|
type ClientKey = string
|
||||||
|
type EncryptedEncryptionKey = string
|
||||||
type EncryptionKey = string
|
type EncryptionKey = string
|
||||||
}
|
}
|
||||||
export {}
|
export {}
|
||||||
|
|||||||
Reference in New Issue
Block a user