fetch decrypt and render jpegs
This commit is contained in:
parent
654aa3e9c4
commit
d5b2b52f95
@ -36,6 +36,15 @@ export async function decrypt_v2(cryptData: {
|
||||
hmac: string;
|
||||
key: string;
|
||||
}): Promise<string> {
|
||||
const md = await decryptBuffer_v2(cryptData);
|
||||
return new TextDecoder().decode(md);
|
||||
}
|
||||
|
||||
export async function decryptBuffer_v2(cryptData: {
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
key: string;
|
||||
}): Promise<ArrayBuffer> {
|
||||
const secret = base64ToArrayBuffer(cryptData.key);
|
||||
const ciphertext_buf = base64ToArrayBuffer(cryptData.ciphertext);
|
||||
const hmac_buf = base64ToArrayBuffer(cryptData.hmac);
|
||||
@ -51,12 +60,12 @@ export async function decrypt_v2(cryptData: {
|
||||
throw Error('Failed HMAC check');
|
||||
}
|
||||
|
||||
const md = await window.crypto.subtle.decrypt(
|
||||
const data = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv: new Uint8Array(16) },
|
||||
await _getAesKey(secret),
|
||||
ciphertext_buf
|
||||
);
|
||||
return new TextDecoder().decode(md);
|
||||
return data;
|
||||
}
|
||||
|
||||
function _getAesKey(secret: ArrayBuffer): Promise<CryptoKey> {
|
||||
|
34
webapp/src/lib/crypto/embedId.ts
Normal file
34
webapp/src/lib/crypto/embedId.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export async function getEmbedId(filename: string): Promise<string> {
|
||||
// generate 64 bit id
|
||||
const idBuf = new Uint32Array((await deriveKey(filename)).slice(0, 8));
|
||||
|
||||
// convert idBuf to base 32 string
|
||||
const id = idBuf.reduce((acc, cur) => {
|
||||
return acc + cur.toString(32);
|
||||
}, '');
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async function deriveKey(seed: string): Promise<ArrayBuffer> {
|
||||
const keyMaterial = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(seed),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits']
|
||||
);
|
||||
|
||||
const masterKey = await window.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: new Uint8Array(16),
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
256
|
||||
);
|
||||
|
||||
return new Uint8Array(masterKey);
|
||||
}
|
@ -1,25 +1,71 @@
|
||||
<script lang="ts">
|
||||
import EmbedIcon from 'svelte-icons/md/MdAttachment.svelte';
|
||||
import FaRegQuestionCircle from 'svelte-icons/fa/FaRegQuestionCircle.svelte';
|
||||
import { EmbedType, getEmbedType, getMimeType } from '$lib/util/embeds';
|
||||
import { onMount } from 'svelte';
|
||||
import { getEmbedId } from '$lib/crypto/embedId';
|
||||
import type { EncryptedEmbed } from '$lib/model/EncryptedEmbed';
|
||||
import { decryptBuffer_v2 } from '$lib/crypto/decrypt';
|
||||
|
||||
export let text: string;
|
||||
let image: HTMLImageElement;
|
||||
let imageUrl: string;
|
||||
|
||||
onMount(async () => {
|
||||
if (getEmbedType(text) === EmbedType.IMAGE) {
|
||||
const encryptedEmbed = await fetchEmbed(text);
|
||||
const embedBuffer = await decryptEmbed(encryptedEmbed);
|
||||
console.log(embedBuffer);
|
||||
imageUrl = renderImage(embedBuffer, text);
|
||||
return () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function renderImage(buffer: ArrayBuffer, filename: string): string {
|
||||
// const bufferView = new Uint8Array(buffer);
|
||||
const blob = new Blob([buffer], { type: getMimeType(filename) });
|
||||
const url = URL.createObjectURL(blob);
|
||||
return url;
|
||||
}
|
||||
|
||||
async function decryptEmbed(embed: EncryptedEmbed): Promise<ArrayBuffer> {
|
||||
const key = location.hash.slice(1);
|
||||
const data = await decryptBuffer_v2({ ...embed, key });
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchEmbed(filename: string): Promise<EncryptedEmbed> {
|
||||
const embedId = await getEmbedId(filename);
|
||||
const response = await fetch(`${location.pathname}/embeds/${embedId}`);
|
||||
if (response.ok) {
|
||||
return (await response.json()) as EncryptedEmbed;
|
||||
}
|
||||
throw new Error(`Failed to fetch embed: ${response.statusText}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<dfn class="not-italic" title="Interal embeds are not shared currently.">
|
||||
<div
|
||||
class="px-4 py-12 border border-zinc-300 dark:border-zinc-600 inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
<span class="h-8 text-zinc-400 ml-0.5 inline-flex items-center whitespace-nowrap gap-1"
|
||||
><span class="w-8 h-8 inline-block">
|
||||
<EmbedIcon />
|
||||
{#if imageUrl}
|
||||
<img bind:this={image} src={imageUrl} alt={text} />
|
||||
{:else}
|
||||
<div>
|
||||
<dfn class="not-italic" title="Interal embeds are not shared currently.">
|
||||
<div
|
||||
class="px-4 py-12 border border-zinc-300 dark:border-zinc-600 inline-flex flex-col items-center justify-center"
|
||||
>
|
||||
<span class="h-8 text-zinc-400 ml-0.5 inline-flex items-center whitespace-nowrap gap-1"
|
||||
><span class="w-8 h-8 inline-block">
|
||||
<EmbedIcon />
|
||||
</span>
|
||||
<span>Internal embed</span>
|
||||
</span>
|
||||
<span>Internal embed</span>
|
||||
</span>
|
||||
<span class="underline cursor-not-allowed inline-flex items-center">
|
||||
<span class="text-[#705dcf] opacity-50">{text}</span>
|
||||
<span class="inline-block w-3 h-3 mb-2 text-zinc-400 ml-0.5"><FaRegQuestionCircle /></span>
|
||||
</span>
|
||||
</div>
|
||||
</dfn>
|
||||
</div>
|
||||
<span class="underline cursor-not-allowed inline-flex items-center">
|
||||
<span class="text-[#705dcf] opacity-50">{text}</span>
|
||||
<span class="inline-block w-3 h-3 mb-2 text-zinc-400 ml-0.5"><FaRegQuestionCircle /></span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</dfn>
|
||||
</div>
|
||||
{/if}
|
||||
|
6
webapp/src/lib/model/EncryptedEmbed.ts
Normal file
6
webapp/src/lib/model/EncryptedEmbed.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type EncryptedEmbed = {
|
||||
note_id: string;
|
||||
embed_id: string;
|
||||
ciphertext: string;
|
||||
hmac: string;
|
||||
};
|
21
webapp/src/lib/util/embeds.ts
Normal file
21
webapp/src/lib/util/embeds.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Returns the EmbedType if embeddable, false if not.
|
||||
* @param filename File extension to check.
|
||||
* @returns EmbedType if embeddable, false if not.
|
||||
*/
|
||||
export function getEmbedType(filename: string): EmbedType | boolean {
|
||||
return isImage(filename) ? EmbedType.IMAGE : false;
|
||||
}
|
||||
|
||||
export function getMimeType(filename: string) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
|
||||
export enum EmbedType {
|
||||
IMAGE = 'IMAGE'
|
||||
}
|
||||
|
||||
function isImage(filename: string): boolean {
|
||||
const match = filename.match(/(png|jpe?g|svg|bmp|gif|)$/i);
|
||||
return !!match && match[0]?.length > 0;
|
||||
}
|
34
webapp/src/routes/note/[note_id]/embeds/[id].ts
Normal file
34
webapp/src/routes/note/[note_id]/embeds/[id].ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { EncryptedEmbed } from '$lib/model/EncryptedEmbed';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async ({ request, clientAddress, params }) => {
|
||||
const ip = (request.headers.get('x-forwarded-for') || clientAddress) as string;
|
||||
const url = `${import.meta.env.VITE_SERVER_INTERNAL}/api/note/${params.note_id}/embeds/${
|
||||
params.id
|
||||
}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'x-forwarded-for': ip
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
try {
|
||||
const embed: EncryptedEmbed = await response.json();
|
||||
return {
|
||||
status: response.status,
|
||||
body: embed
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
status: 500,
|
||||
error: response.statusText
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: response.status,
|
||||
error: response.statusText
|
||||
};
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user