From dc8e576679cd893f7c645a1de25f0fadbc0d66c0 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Mon, 26 Oct 2026 20:06:54 +0100 Subject: [PATCH] add password prompt to ZK viewer for wrapped uploads --- frontend/src/stores/view.ts | 35 ++++++++++++++++- frontend/src/views/ViewerView.vue | 62 +++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/frontend/src/stores/view.ts b/frontend/src/stores/view.ts index 476d07c..bef8188 100644 --- a/frontend/src/stores/view.ts +++ b/frontend/src/stores/view.ts @@ -4,6 +4,7 @@ import { api } from '@/plugins/axios'; import type { GetZkUploadCiphertextResult } from '@/types/upload/GetZkUploadCiphertextResult'; import { base64ToBytes } from '@/utils/encoding/base64ToBytes'; import { toArrayBuffer } from '@/utils/toArrayBuffer'; +import { decryptPasswordProtectedZkUpload } from '@/utils/upload/decryptPasswordProtectedZkUpload'; import { decryptZkUpload } from '@/utils/upload/decryptZkUpload'; export const useViewStore = defineStore('view', () => { @@ -22,13 +23,43 @@ export const useViewStore = defineStore('view', () => { return data; }; - const loadZk = async (payload: GetZkUploadCiphertextResult, fragmentKey: string): Promise => { + const loadZk = async ( + payload: GetZkUploadCiphertextResult, + fragmentKey: string, + password?: string + ): Promise => { clearImage(); const iv = base64ToBytes(payload.iv); const ciphertext = base64ToBytes(payload.ciphertext); - const plaintext = await decryptZkUpload(ciphertext, iv, fragmentKey); + let plaintext: Uint8Array; + + if (payload.passwordProtected) { + if (!password) { + throw new Error('Password required'); + } + + if (!payload.wrappedDek || !payload.wrapIv || !payload.salt) { + throw new Error('Incomplete password protection data'); + } + + const wrappedDek = base64ToBytes(payload.wrappedDek); + const wrapIv = base64ToBytes(payload.wrapIv); + const salt = base64ToBytes(payload.salt); + + plaintext = await decryptPasswordProtectedZkUpload( + ciphertext, + iv, + fragmentKey, + password, + wrappedDek, + wrapIv, + salt + ); + } else { + plaintext = await decryptZkUpload(ciphertext, iv, fragmentKey); + } const buffer = toArrayBuffer(plaintext); diff --git a/frontend/src/views/ViewerView.vue b/frontend/src/views/ViewerView.vue index 440248f..c60e38e 100644 --- a/frontend/src/views/ViewerView.vue +++ b/frontend/src/views/ViewerView.vue @@ -2,6 +2,29 @@

Loading image...

+
+ + +

{{ unlockError }}

+ + +
+
@@ -11,12 +34,18 @@ import { storeToRefs } from 'pinia'; import { onBeforeMount, onBeforeUnmount, ref } from 'vue'; import { useRoute } from 'vue-router'; import { useViewStore } from '@/stores/view'; +import type { GetZkUploadCiphertextResult } from '@/types/upload/GetZkUploadCiphertextResult'; const route = useRoute(); const viewStore = useViewStore(); const { imageObjectUrl } = storeToRefs(viewStore); const fetching = ref(false); +const unlocking = ref(false); +const zkPayload = ref(null); +const fragmentKey = ref(''); +const password = ref(''); +const unlockError = ref(''); onBeforeMount(async () => { const uploadId = route.params.id; @@ -25,22 +54,24 @@ onBeforeMount(async () => { return; } - const fragmentKey = route.hash.slice(1); + const key = route.hash.slice(1); - if (!fragmentKey) { + if (!key) { return; } + fragmentKey.value = key; fetching.value = true; try { const payload = await viewStore.fetchZkCiphertext(uploadId); if (payload.passwordProtected) { + zkPayload.value = payload; return; } - await viewStore.loadZk(payload, fragmentKey); + await viewStore.loadZk(payload, key); } catch (error) { console.error('[viewer] load failed', { uploadId, error }); } finally { @@ -48,6 +79,31 @@ onBeforeMount(async () => { } }); +const onUnlockSubmit = async (): Promise => { + const payload = zkPayload.value; + const key = fragmentKey.value; + + if (!payload || !key) { + return; + } + + unlocking.value = true; + unlockError.value = ''; + + try { + await viewStore.loadZk(payload, key, password.value); + + zkPayload.value = null; + password.value = ''; + } catch (error) { + console.error('[viewer] unlock failed', { error }); + + unlockError.value = 'Wrong password'; + } finally { + unlocking.value = false; + } +}; + onBeforeUnmount(() => { viewStore.clearImage(); });