enable/disable e2e encryption

This commit is contained in:
2023-05-28 21:45:47 +02:00
parent 77f5bafa2f
commit b4eab2d8e8
9 changed files with 372 additions and 59 deletions

View File

@@ -17,7 +17,7 @@ const emit = defineEmits<{
<template #activator="{ open }">
<UIButton size="sm" @click="open"><i class="fas fa-fw fa-trash" /></UIButton>
</template>
<template #title>Delete note</template>
<template #title><i class="fas fa-fw fa-trash mr-2" />Delete note</template>
<template #default>Are you sure you want to delete this note?</template>
<template #actions="{ close }">
<UIButton size="sm" color="primary" @click="emit('delete', close)">Delete notes</UIButton>
@@ -28,7 +28,7 @@ const emit = defineEmits<{
<template #activator="{ open }">
<UIButton size="sm" @click="open"><i class="fas fa-fw fa-sitemap" /></UIButton>
</template>
<template #title>Set root note</template>
<template #title><i class="fas fa-fw fa-sitemap mr-2" />Set root note</template>
<template #default>Are you sure you want to set this note as root note?</template>
<template #actions="{ close }">
<UIButton size="sm" @click="close">Cancel</UIButton>

View File

@@ -1,6 +1,10 @@
<script setup lang="ts">
import { user } from '@/composables/useFirebase'
import { encryptionKey, enableEncryption, disableEncryption } from '@/composables/useEncryption'
import { notes } from '@/composables/useNotes'
import { format } from 'date-fns'
import JSZip from 'jszip'
import FileSaver from 'file-saver'
const verificationEmailSent = ref(false)
const sendVerificationMail = () => {
@@ -8,18 +12,49 @@ const sendVerificationMail = () => {
user.value.sendEmailVerification()
verificationEmailSent.value = true
}
const exportNotes = async () => {
const zip = new JSZip()
notes.value.forEach((note) => {
zip.file(`${note.title}-${note.id}.md`, note.content)
})
const blob = await zip.generateAsync({ type: 'blob' })
const currentDate = format(new Date(), 'yyyyMMdd')
FileSaver.saveAs(blob, `contexted-${user.value?.email}-${currentDate}.zip`)
}
const showEncryptionDialog = ref(false)
watch(showEncryptionDialog, () => {
passphrase.value = ''
})
const passphrase = ref('')
const toggleEncryptionError = ref('')
const encryptionEnabled = computed(() => Boolean(encryptionKey.value))
const toggleEncryption = async () => {
const result = encryptionEnabled.value
? await disableEncryption(passphrase.value)
: await enableEncryption(passphrase.value)
if (typeof result === 'string') {
toggleEncryptionError.value = result
} else {
toggleEncryptionError.value = ''
showEncryptionDialog.value = false
}
}
</script>
<template>
<UIModal size="lg">
<template #activator="{ open }">
<UIDropdownItem @click="open">
<i class="fa-fw fa-solid fa-sliders" />
Settings
Account settings
</UIDropdownItem>
</template>
<template #title>
<i class="fa-fw fa-solid fa-sliders mr-2" />
Settings
Account settings
</template>
<template #default>
<div class="space-y-2">
@@ -62,17 +97,75 @@ const sendVerificationMail = () => {
<UICard>
<template #title>Notes</template>
<template #default>
<div class="w-full flex-row sm:flex items-center">
<div class="items-top w-full flex-row sm:flex">
<div class="font-bold sm:w-4/12">Export notes</div>
<UIButton size="sm" color="secondary"><i class="fa-fw fa-solid fa-file-export mr-2"></i>Export notes</UIButton>
<UIButton size="sm" @click="exportNotes">
<i class="fa-fw fa-solid fa-file-export mr-2"></i>
Export notes
</UIButton>
</div>
<div class="w-full flex-row sm:flex items-center">
<div class="items-top w-full flex-row sm:flex">
<div class="font-bold sm:w-4/12">Delete account</div>
<UIButton size="sm" color="error"><i class="fa-fw fa-solid fa-trash mr-2"></i>Delete account</UIButton>
<UIButton size="sm" color="error">
<i class="fa-fw fa-solid fa-trash mr-2"></i>
Delete account
</UIButton>
</div>
<div class="w-full flex-row sm:flex items-center">
<div class="items-top w-full flex-row sm:flex">
<div class="font-bold sm:w-4/12">End-to-end encryption</div>
<UIButton size="sm" color="secondary"><i class="fa-fw fa-solid fa-key mr-2"></i>Enable end-to-end encryption</UIButton>
<div>
<template v-if="!encryptionEnabled">
<UIButton
size="sm"
@click="showEncryptionDialog = true"
v-if="showEncryptionDialog === false"
>
<i class="fa-fw fa-solid fa-key mr-2"></i>
Enable end-to-end encryption
</UIButton>
</template>
<template v-else>
<UIButton
size="sm"
@click="showEncryptionDialog = true"
v-if="showEncryptionDialog === false"
>
<i class="fa-fw fa-solid fa-key mr-2"></i>
Disable end-to-end encryption
</UIButton>
</template>
<UIAlert color="info" density="compact" v-if="showEncryptionDialog">
<div class="space-y-2">
<div>
Enter your passphrase to
{{ encryptionEnabled ? 'disable' : 'enable' }}
encryption
</div>
<UIInputText
size="sm"
type="password"
:color="toggleEncryptionError ? 'error' : 'regular'"
v-model="passphrase"
class="w-full"
/>
<UIAlert density="compact" color="error" v-if="toggleEncryptionError">
<i class="fa-solid fa-triangle-exclamation"></i>
{{ toggleEncryptionError }}
</UIAlert>
<div class="flex justify-end space-x-2">
<UIButton size="sm" @click="showEncryptionDialog = false">Close</UIButton>
<UIButton
:disabled="passphrase.length === 0"
size="sm"
color="primary"
@click="toggleEncryption"
>
{{ encryptionEnabled ? 'Disable' : 'Enable' }} encryption
</UIButton>
</div>
</div>
</UIAlert>
</div>
</div>
</template>
</UICard>

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
interface Props {
color?: 'info' | 'success' | 'warning' | 'error'
density?: 'regular' | 'compact'
}
const props = withDefaults(defineProps<Props>(), {
color: 'info'
color: 'info',
density: 'regular'
})
const styleClass = computed(() => {
@@ -13,12 +15,17 @@ const styleClass = computed(() => {
'warning': 'dui-alert-warning',
'error': 'dui-alert-error'
}
const densityVariants = {
'regular': 'py-4 px-4',
'compact': 'py-2 px-4'
}
const colorClass = colorVariants[props.color]
return [colorClass]
const densityClass = densityVariants[props.density]
return [colorClass, densityClass]
})
</script>
<template>
<div class="dui-alert shadow-lg items-start" :class="styleClass">
<div class="flex items-center"><slot></slot></div>
<div class="flex items-center w-full"><slot></slot></div>
</div>
</template>

View File

@@ -6,6 +6,7 @@ const props = withDefaults(
open?: boolean
persistent?: boolean
size?: 'sm' | 'md' | 'lg'
icon?: string
}>(),
{
open: false,

View File

@@ -1,6 +1,8 @@
import { doc, getDoc } from 'firebase/firestore'
import { doc, getDoc, setDoc } from 'firebase/firestore'
import { user, db } from '@/composables/useFirebase'
import { decrypt, encrypt, calculateClientKey } from '@/utils/crypto'
import { decrypt, encrypt, calculateClientKey, generateEncryptionKey } from '@/utils/crypto'
import { preferredNotesSource } from '@/composables/useSettings'
import { activeNotesSource, syncNotesToFirebase, baseNotes } from '@/composables/useNotes'
function getClientKeysFromLocalStorage(): { [uid: string]: string } {
try {
@@ -42,20 +44,38 @@ export const verifyClientKey = (clientKey: ClientKey) => {
}
}
const encryptedEncryptionKey = ref<EncryptedEncryptionKey>()
const removeClientKey = () => {
if (!user.value) return
const clientKeys = structuredClone(getClientKeysFromLocalStorage())
delete clientKeys[user.value?.uid]
localStorage.setItem('clientKeys', JSON.stringify(clientKeys))
}
const encryptedEncryptionKey = ref<EncryptedEncryptionKey | null>()
async function getEncryptedEncryptionKey(): Promise<EncryptedEncryptionKey | void> {
if (!user.value || !db.value) return
const data = (await getDoc(doc(db.value, 'encryptionKeys', user.value?.uid || ''))).data()
const data = (await getDoc(doc(db.value, 'encryptionKeys', user.value.uid))).data()
return data?.key
}
async function setEncryptedEncryptionKey(
encryptedEncryptionKey: EncryptedEncryptionKey | null
): Promise<void> {
if (!user.value || !db.value) return
const docRef = doc(db.value, 'encryptionKeys', user.value.uid)
await setDoc(docRef, { key: encryptedEncryptionKey })
}
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)
if (!encryptedEncryptionKey.value || !clientKey.value) {
encryptionKey.value = null
} else {
encryptionKey.value = decrypt(encryptedEncryptionKey.value, clientKey.value)
}
}
export const passphraseRequired = computed(() => {
@@ -63,10 +83,15 @@ export const passphraseRequired = computed(() => {
})
const decryptNote = (note: BaseNote, key: EncryptionKey) => {
return {
...note,
title: decrypt(note.title, key),
content: decrypt(note.content, key)
try {
return {
...note,
title: decrypt(note.title, key),
content: decrypt(note.content, key)
}
} catch (error: any) {
console.error(error)
return note
}
}
@@ -91,3 +116,34 @@ export const encryptNotes = (notes: BaseNotes, encryptionKey: EncryptionKey) =>
)
return encryptedNotes
}
export const verifyPassphrase = (passphrase: string) => {
const calculatedClientKey = calculateClientKey(passphrase)
return calculatedClientKey === clientKey.value
}
export const disableEncryption = async (passphrase: string) => {
if (!encryptionKey.value) return "Encryption key doesn't exist."
if (!verifyPassphrase(passphrase)) return 'Passphrase is incorrect.'
preferredNotesSource.value = 'firebase'
if (activeNotesSource.value !== 'firebase') throw Error('Something went wrong.')
await setEncryptedEncryptionKey(null)
encryptedEncryptionKey.value = null
encryptionKey.value = undefined
removeClientKey()
await syncNotesToFirebase(baseNotes.value)
getEncryptionKey()
}
export const enableEncryption = async (passphrase: string) => {
preferredNotesSource.value = 'firebase'
if (activeNotesSource.value !== 'firebase') throw Error('Something went wrong.')
const candidateEncryptionKey = generateEncryptionKey()
const candidateClientKey = calculateClientKey(passphrase)
const candidateEncryptedEncryptionKey = encrypt(candidateEncryptionKey, candidateClientKey)
await setEncryptedEncryptionKey(candidateEncryptedEncryptionKey)
encryptedEncryptionKey.value = candidateEncryptedEncryptionKey
encryptionKey.value = candidateEncryptionKey
setClientKey(passphrase)
syncNotesToFirebase(baseNotes.value)
}

View File

@@ -39,36 +39,42 @@ watchEffect(() => {
activeNotesSource.value = getSource()
})
const baseNotes = ref<BaseNotes>({})
export const baseNotes = ref<BaseNotes>({})
const syncNotesLocal = (notes: BaseNotes) => {
localStorage.setItem('notes', JSON.stringify(notes))
}
export const syncNotesToFirebase = async (newNotes: BaseNotes, oldNotes?: BaseNotes) => {
if (!db.value) throw Error("Database undefined, can't sync to Firebase")
if (!user.value) throw Error("User undefined, can't sync to Firebase")
const notes = encryptionKey.value
? encryptNotes(baseNotes.value, encryptionKey.value)
: baseNotes.value
try {
const docRef = doc(db.value, 'pages', user.value.uid)
if (oldNotes) {
const notesToDelete = Object.keys(oldNotes).filter((x) => !Object.keys(newNotes).includes(x))
await Promise.all(
notesToDelete.map((noteId: string) => {
return updateDoc(docRef, { [noteId]: deleteField() })
})
)
}
await updateDoc(docRef, notes)
} catch (error: any) {
console.error(error)
}
}
watch(
baseNotes,
async (newBaseNotes, oldBaseNotes) => {
if (!activeNotesSource.value || Object.keys(baseNotes.value).length === 0) return
console.log()
if (activeNotesSource.value === 'local') {
localStorage.setItem('notes', JSON.stringify(baseNotes.value))
syncNotesLocal(baseNotes.value)
} else if (activeNotesSource.value === 'firebase') {
if (!db.value) throw Error("Database undefined, can't sync to Firebase")
if (!user.value) throw Error("User undefined, can't sync to Firebase")
const notes = encryptionKey.value
? encryptNotes(baseNotes.value, encryptionKey.value)
: baseNotes.value
const notesToDelete = Object.keys(oldBaseNotes).filter(
(x) => !Object.keys(newBaseNotes).includes(x)
)
try {
const docRef = doc(db.value, 'pages', user.value.uid)
await Promise.all(
notesToDelete.map((noteId: string) => {
return updateDoc(docRef, { [noteId]: deleteField() })
})
)
await updateDoc(docRef, notes)
} catch (error: any) {
console.error(error)
}
syncNotesToFirebase(newBaseNotes, oldBaseNotes)
}
},
{ deep: true }

View File

@@ -17,3 +17,7 @@ export const decrypt = (encryptedMessage: string, key: string): string => {
export const encrypt = (unencryptedMessage: string, key: string): string => {
return CryptoJS.AES.encrypt(encryptionPrefix + unencryptedMessage, key).toString()
}
export const generateEncryptionKey = () => {
return CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Hex)
}