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;
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> {

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,10 +1,54 @@
<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>
{#if imageUrl}
<img bind:this={image} src={imageUrl} alt={text} />
{:else}
<div>
<dfn class="not-italic" title="Interal embeds are not shared currently.">
<div
@ -18,8 +62,10 @@
</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 class="inline-block w-3 h-3 mb-2 text-zinc-400 ml-0.5"><FaRegQuestionCircle /></span
>
</span>
</div>
</dfn>
</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
};
}
};