fetch decrypt and render jpegs

This commit is contained in:
Maxime Cannoodt 2022-09-13 23:21:26 +02:00
parent 654aa3e9c4
commit d5b2b52f95
6 changed files with 169 additions and 19 deletions

View File

@ -36,6 +36,15 @@ export async function decrypt_v2(cryptData: {
hmac: string; hmac: string;
key: string; key: string;
}): Promise<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 secret = base64ToArrayBuffer(cryptData.key);
const ciphertext_buf = base64ToArrayBuffer(cryptData.ciphertext); const ciphertext_buf = base64ToArrayBuffer(cryptData.ciphertext);
const hmac_buf = base64ToArrayBuffer(cryptData.hmac); const hmac_buf = base64ToArrayBuffer(cryptData.hmac);
@ -51,12 +60,12 @@ export async function decrypt_v2(cryptData: {
throw Error('Failed HMAC check'); 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) }, { name: 'AES-CBC', iv: new Uint8Array(16) },
await _getAesKey(secret), await _getAesKey(secret),
ciphertext_buf ciphertext_buf
); );
return new TextDecoder().decode(md); return data;
} }
function _getAesKey(secret: ArrayBuffer): Promise<CryptoKey> { function _getAesKey(secret: ArrayBuffer): Promise<CryptoKey> {

View 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);
}

View File

@ -1,25 +1,71 @@
<script lang="ts"> <script lang="ts">
import EmbedIcon from 'svelte-icons/md/MdAttachment.svelte'; import EmbedIcon from 'svelte-icons/md/MdAttachment.svelte';
import FaRegQuestionCircle from 'svelte-icons/fa/FaRegQuestionCircle.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; 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> </script>
<div> {#if imageUrl}
<dfn class="not-italic" title="Interal embeds are not shared currently."> <img bind:this={image} src={imageUrl} alt={text} />
<div {:else}
class="px-4 py-12 border border-zinc-300 dark:border-zinc-600 inline-flex flex-col items-center justify-center" <div>
> <dfn class="not-italic" title="Interal embeds are not shared currently.">
<span class="h-8 text-zinc-400 ml-0.5 inline-flex items-center whitespace-nowrap gap-1" <div
><span class="w-8 h-8 inline-block"> class="px-4 py-12 border border-zinc-300 dark:border-zinc-600 inline-flex flex-col items-center justify-center"
<EmbedIcon /> >
<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>
<span>Internal embed</span> <span class="underline cursor-not-allowed inline-flex items-center">
</span> <span class="text-[#705dcf] opacity-50">{text}</span>
<span class="underline cursor-not-allowed inline-flex items-center"> <span class="inline-block w-3 h-3 mb-2 text-zinc-400 ml-0.5"><FaRegQuestionCircle /></span
<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>
</span> </div>
</div> </dfn>
</dfn> </div>
</div> {/if}

View File

@ -0,0 +1,6 @@
export type EncryptedEmbed = {
note_id: string;
embed_id: string;
ciphertext: string;
hmac: string;
};

View 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;
}

View 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
};
}
};