add password prompt to ZK viewer for wrapped uploads
This commit is contained in:
@@ -4,6 +4,7 @@ import { api } from '@/plugins/axios';
|
|||||||
import type { GetZkUploadCiphertextResult } from '@/types/upload/GetZkUploadCiphertextResult';
|
import type { GetZkUploadCiphertextResult } from '@/types/upload/GetZkUploadCiphertextResult';
|
||||||
import { base64ToBytes } from '@/utils/encoding/base64ToBytes';
|
import { base64ToBytes } from '@/utils/encoding/base64ToBytes';
|
||||||
import { toArrayBuffer } from '@/utils/toArrayBuffer';
|
import { toArrayBuffer } from '@/utils/toArrayBuffer';
|
||||||
|
import { decryptPasswordProtectedZkUpload } from '@/utils/upload/decryptPasswordProtectedZkUpload';
|
||||||
import { decryptZkUpload } from '@/utils/upload/decryptZkUpload';
|
import { decryptZkUpload } from '@/utils/upload/decryptZkUpload';
|
||||||
|
|
||||||
export const useViewStore = defineStore('view', () => {
|
export const useViewStore = defineStore('view', () => {
|
||||||
@@ -22,13 +23,43 @@ export const useViewStore = defineStore('view', () => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadZk = async (payload: GetZkUploadCiphertextResult, fragmentKey: string): Promise<void> => {
|
const loadZk = async (
|
||||||
|
payload: GetZkUploadCiphertextResult,
|
||||||
|
fragmentKey: string,
|
||||||
|
password?: string
|
||||||
|
): Promise<void> => {
|
||||||
clearImage();
|
clearImage();
|
||||||
|
|
||||||
const iv = base64ToBytes(payload.iv);
|
const iv = base64ToBytes(payload.iv);
|
||||||
const ciphertext = base64ToBytes(payload.ciphertext);
|
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);
|
const buffer = toArrayBuffer(plaintext);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,29 @@
|
|||||||
<main class="flex min-h-screen items-center justify-center p-4">
|
<main class="flex min-h-screen items-center justify-center p-4">
|
||||||
<p v-if="fetching" class="text-sm text-muted-foreground">Loading image...</p>
|
<p v-if="fetching" class="text-sm text-muted-foreground">Loading image...</p>
|
||||||
|
|
||||||
|
<form v-else-if="zkPayload" class="flex w-full max-w-sm flex-col gap-3" @submit.prevent="onUnlockSubmit">
|
||||||
|
<label class="flex flex-col gap-1.5 text-sm">
|
||||||
|
<span>Password</span>
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||||
|
:disabled="unlocking"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p v-if="unlockError" class="text-sm text-destructive">{{ unlockError }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:disabled="unlocking || !password"
|
||||||
|
>
|
||||||
|
{{ unlocking ? 'Unlocking...' : 'View image' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<img v-else-if="imageObjectUrl" :src="imageObjectUrl" class="max-h-[80vh] max-w-full object-contain" />
|
<img v-else-if="imageObjectUrl" :src="imageObjectUrl" class="max-h-[80vh] max-w-full object-contain" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
@@ -11,12 +34,18 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { onBeforeMount, onBeforeUnmount, ref } from 'vue';
|
import { onBeforeMount, onBeforeUnmount, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useViewStore } from '@/stores/view';
|
import { useViewStore } from '@/stores/view';
|
||||||
|
import type { GetZkUploadCiphertextResult } from '@/types/upload/GetZkUploadCiphertextResult';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const viewStore = useViewStore();
|
const viewStore = useViewStore();
|
||||||
const { imageObjectUrl } = storeToRefs(viewStore);
|
const { imageObjectUrl } = storeToRefs(viewStore);
|
||||||
|
|
||||||
const fetching = ref(false);
|
const fetching = ref(false);
|
||||||
|
const unlocking = ref(false);
|
||||||
|
const zkPayload = ref<GetZkUploadCiphertextResult | null>(null);
|
||||||
|
const fragmentKey = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const unlockError = ref('');
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
const uploadId = route.params.id;
|
const uploadId = route.params.id;
|
||||||
@@ -25,22 +54,24 @@ onBeforeMount(async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fragmentKey = route.hash.slice(1);
|
const key = route.hash.slice(1);
|
||||||
|
|
||||||
if (!fragmentKey) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragmentKey.value = key;
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await viewStore.fetchZkCiphertext(uploadId);
|
const payload = await viewStore.fetchZkCiphertext(uploadId);
|
||||||
|
|
||||||
if (payload.passwordProtected) {
|
if (payload.passwordProtected) {
|
||||||
|
zkPayload.value = payload;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await viewStore.loadZk(payload, fragmentKey);
|
await viewStore.loadZk(payload, key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[viewer] load failed', { uploadId, error });
|
console.error('[viewer] load failed', { uploadId, error });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -48,6 +79,31 @@ onBeforeMount(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onUnlockSubmit = async (): Promise<void> => {
|
||||||
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
viewStore.clearImage();
|
viewStore.clearImage();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user