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