Fix encrypted audio attachment playback (#610)
This commit is contained in:
parent
8cc970906d
commit
d67309650c
4 changed files with 91 additions and 18 deletions
|
|
@ -22,6 +22,7 @@
|
|||
import Link from "@lib/components/Link.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
import ContentLinkBlockAudio from "@app/components/ContentLinkBlockAudio.svelte"
|
||||
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
|
||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
{#if isRoomOrRelay}
|
||||
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
|
||||
{:else if isAudio}
|
||||
<audio controls src={url} preload="metadata" class="my-2 w-full max-w-xl"></audio>
|
||||
<ContentLinkBlockAudio {url} {event} />
|
||||
{:else if isVideo}
|
||||
<Link {external} {href} class="my-2 block">
|
||||
<video
|
||||
|
|
|
|||
57
src/app/components/ContentLinkBlockAudio.svelte
Normal file
57
src/app/components/ContentLinkBlockAudio.svelte
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
import {onDestroy} from "svelte"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import {displayUrl} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import {decryptUrl, getUrlTags} from "@app/content"
|
||||
|
||||
type Props = {url: string; event: TrustedEvent}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
const meta = getUrlTags(url, event)
|
||||
const algorithm = tagValue(tagSpec("encryption-algorithm"), meta)
|
||||
const controller = new AbortController()
|
||||
|
||||
let source = $state<string>(algorithm ? "" : url)
|
||||
let failed = $state(false)
|
||||
let loading = $state(false)
|
||||
|
||||
const load = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
source = await decryptUrl(url, event, controller.signal)
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
console.error(error)
|
||||
failed = true
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
failed = true
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
controller.abort()
|
||||
if (source?.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(source)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if failed}
|
||||
<a href={url} class="link-content whitespace-nowrap">{displayUrl(url)}</a>
|
||||
{:else if source}
|
||||
<audio controls src={source} preload="metadata" class="my-2 w-full max-w-xl" onerror={onError}
|
||||
></audio>
|
||||
{:else}
|
||||
<Button class="button button-link my-2" disabled={loading} onclick={load}>
|
||||
<Spinner {loading}>Load audio</Spinner>
|
||||
</Button>
|
||||
{/if}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {displayUrl, once} from "@welshman/lib"
|
||||
import {getBlob, decryptFile, makeBlossomAuthEvent, tagSpec, tagValue} from "@welshman/util"
|
||||
import {getBlob, makeBlossomAuthEvent, tagSpec, tagValue} from "@welshman/util"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {user} from "@app/core"
|
||||
import {getUrlTags} from "@app/content"
|
||||
import {decryptUrl, getUrlTags} from "@app/content"
|
||||
|
||||
const {value, event, ...props} = $props()
|
||||
|
||||
|
|
@ -14,10 +14,7 @@
|
|||
|
||||
// Fallback to filename if hash was omitted from the message for interoperability
|
||||
const hash = tagValue(tagSpec("x"), meta) || url.split(/[\/\.]/).slice(-2)[0]
|
||||
const key = tagValue(tagSpec("decryption-key"), meta)
|
||||
const nonce = tagValue(tagSpec("decryption-nonce"), meta)
|
||||
const algorithm = tagValue(tagSpec("encryption-algorithm"), meta)
|
||||
const mime = tagValue(tagSpec("m"), meta)
|
||||
const controller = new AbortController()
|
||||
const fileName =
|
||||
tagValue(tagSpec("filename"), meta) ||
|
||||
tagValue(tagSpec("name"), meta) ||
|
||||
|
|
@ -57,23 +54,18 @@
|
|||
let src = $state("")
|
||||
|
||||
onMount(async () => {
|
||||
// If we have an encryption algorithm, fetch and decrypt
|
||||
if (algorithm === "aes-gcm" && key && nonce) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const ciphertext = new Uint8Array(await response.arrayBuffer())
|
||||
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
|
||||
|
||||
setBlobSrc(new Uint8Array(decryptedData), mime)
|
||||
} catch {
|
||||
try {
|
||||
src = await decryptUrl(url, event, controller.signal)
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted) {
|
||||
console.error(error)
|
||||
hasError = true
|
||||
}
|
||||
} else {
|
||||
src = url
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
controller.abort()
|
||||
revokeSrc()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
ZAP_GOAL,
|
||||
ZAP_RECEIPT,
|
||||
matchTags,
|
||||
decryptFile,
|
||||
tagSpec,
|
||||
tagValue,
|
||||
} from "@welshman/util"
|
||||
|
|
@ -57,6 +58,28 @@ export const getUrlContentType = (url: string, event: TrustedEvent) => {
|
|||
return tagValue(tagSpec("m"), tags) || tagValue(tagSpec("file-type"), tags) || ""
|
||||
}
|
||||
|
||||
export const decryptUrl = async (url: string, event: TrustedEvent, signal?: AbortSignal) => {
|
||||
const tags = getUrlTags(url, event)
|
||||
const algorithm = tagValue(tagSpec("encryption-algorithm"), tags)
|
||||
const key = tagValue(tagSpec("decryption-key"), tags)
|
||||
const nonce = tagValue(tagSpec("decryption-nonce"), tags)
|
||||
|
||||
if (algorithm === "aes-gcm" && key && nonce) {
|
||||
const response = await fetch(url, {signal})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Attachment download failed (HTTP ${response.status}).`)
|
||||
}
|
||||
const ciphertext = new Uint8Array(await response.arrayBuffer())
|
||||
const data = await decryptFile({ciphertext, key, nonce, algorithm})
|
||||
signal?.throwIfAborted()
|
||||
return URL.createObjectURL(
|
||||
new Blob([new Uint8Array(data)], {type: getUrlContentType(url, event)}),
|
||||
)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export const makeCommentFilter = (kinds: number[], extra: Filter = {}) => ({
|
||||
kinds: [COMMENT],
|
||||
"#K": kinds.map(String),
|
||||
|
|
|
|||
Loading…
Reference in a new issue