[#115] Implement Binary File Attachments (#116)

This commit is contained in:
Knut Ahlers 2023-10-02 21:52:24 +02:00 committed by GitHub
parent a098395daf
commit c5124731f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 330 additions and 31 deletions

View file

@ -29,7 +29,7 @@
class="row"
@submit.prevent="createSecret"
>
<div class="col-12 mb-3 order-0">
<div class="col-12 mb-3">
<label for="createSecretData">{{ $t('label-secret-data') }}</label>
<textarea
id="createSecretData"
@ -38,13 +38,43 @@
rows="5"
/>
</div>
<div
v-if="!$root.customize.disableFileAttachment"
class="col-12 mb-3"
>
<label for="createSecretFiles">{{ $t('label-secret-files') }}</label>
<input
id="createSecretFiles"
ref="createSecretFiles"
class="form-control"
type="file"
multiple
:accept="$root.customize.acceptedFileTypes"
@change="updateFileSize"
>
<div class="form-text">
{{ $t('text-max-filesize', { maxSize: bytesToHuman(maxFileSize) }) }}
</div>
<div
v-if="maxFileSizeExceeded"
class="alert alert-danger"
>
{{ $t('text-max-filesize-exceeded', { curSize: bytesToHuman(fileSize), maxSize: bytesToHuman(maxFileSize) }) }}
</div>
</div>
<div class="col-md-6 col-12 order-2 order-md-1">
<button
type="submit"
class="btn btn-success"
:disabled="secret.trim().length < 1"
:disabled="secret.trim().length < 1 || maxFileSizeExceeded || createRunning"
>
{{ $t('btn-create-secret') }}
<template v-if="!createRunning">
{{ $t('btn-create-secret') }}
</template>
<template v-else>
<i class="fa-solid fa-spinner fa-spin-pulse" />
{{ $t('btn-create-secret-processing') }}
</template>
</button>
</div>
<div
@ -80,6 +110,8 @@
/* global maxSecretExpire */
import appCrypto from '../crypto.js'
import { bytesToHuman } from '../helpers'
import OTSMeta from '../ots-meta'
const defaultExpiryChoices = [
90 * 86400, // 90 days
@ -94,6 +126,17 @@ const defaultExpiryChoices = [
5 * 60, // 5 minutes
]
/*
* We define an internal max file-size which cannot get exceeded even
* though the server might accept more: at around 70 MiB the base64
* encoding broke and nothing works anymore. This might be fixed by
* changing how the base64 implementation works (maybe use a WASM
* object?) or switching to a browser-native implementation in case
* that will appear somewhen in the future but for now we just "fix"
* the issue by disallowing bigger files.
*/
const internalMaxFileSize = 64 * 1024 * 1024 // 64 MiB
const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const passwordLength = 20
@ -122,6 +165,14 @@ export default {
return choices
},
maxFileSize() {
return this.$root.customize.maxAttachmentSizeTotal === 0 ? internalMaxFileSize : Math.min(internalMaxFileSize, this.$root.customize.maxAttachmentSizeTotal)
},
maxFileSizeExceeded() {
return this.fileSize > this.maxFileSize
},
},
created() {
@ -131,6 +182,8 @@ export default {
data() {
return {
canWrite: null,
createRunning: false,
fileSize: 0,
secret: '',
securePassword: null,
selectedExpiry: null,
@ -138,6 +191,8 @@ export default {
},
methods: {
bytesToHuman,
checkWriteAccess() {
fetch('api/isWritable', {
credentials: 'same-origin',
@ -157,14 +212,28 @@ export default {
// createSecret executes the secret creation after encrypting the secret
createSecret() {
if (this.secret.trim().length < 1) {
if (this.secret.trim().length < 1 || this.maxFileSizeExceeded) {
return false
}
// Encoding large files takes a while, prevent duplicate click on "create"
this.createRunning = true
this.securePassword = [...window.crypto.getRandomValues(new Uint8Array(passwordLength))]
.map(n => passwordCharset[n % passwordCharset.length])
.join('')
appCrypto.enc(this.secret, this.securePassword)
const meta = new OTSMeta()
meta.secret = this.secret
if (this.$refs.createSecretFiles) {
for (const f of [...this.$refs.createSecretFiles.files]) {
meta.files.push(f)
}
}
meta.serialize()
.then(secret => appCrypto.enc(secret, this.securePassword))
.then(secret => {
let reqURL = 'api/create'
if (this.selectedExpiry !== null) {
@ -205,6 +274,15 @@ export default {
return false
},
updateFileSize() {
let cumSize = 0
for (const f of [...this.$refs.createSecretFiles.files]) {
cumSize += f.size
}
this.fileSize = cumSize
},
},
name: 'AppCreate',

View file

@ -10,9 +10,16 @@
<p v-html="$t('text-pre-reveal-hint')" />
<button
class="btn btn-success"
:disabled="secretLoading"
@click="requestSecret"
>
{{ $t('btn-reveal-secret') }}
<template v-if="!secretLoading">
{{ $t('btn-reveal-secret') }}
</template>
<template v-else>
<i class="fa-solid fa-spinner fa-spin-pulse" />
{{ $t('btn-reveal-secret-processing') }}
</template>
</button>
</template>
<template v-else>
@ -41,6 +48,22 @@
</div>
</div>
<p v-html="$t('text-hint-burned')" />
<template v-if="files.length > 0">
<p v-html="$t('text-attached-files')" />
<ul>
<li
v-for="file in files"
:key="file.name"
class="font-monospace"
>
<a
:href="file.url"
:download="file.name"
>{{ file.name }}</a>
({{ bytesToHuman(file.size) }})
</li>
</ul>
</template>
</template>
</div>
</div>
@ -49,21 +72,28 @@
import appClipboardButton from './clipboard-button.vue'
import appCrypto from '../crypto.js'
import appQrButton from './qr-button.vue'
import { bytesToHuman } from '../helpers'
import OTSMeta from '../ots-meta'
export default {
components: { appClipboardButton, appQrButton },
data() {
return {
files: [],
popover: null,
secret: null,
secretContentBlobURL: null,
secretLoading: false,
}
},
methods: {
bytesToHuman,
// requestSecret requests the encrypted secret from the backend
requestSecret() {
this.secretLoading = true
window.history.replaceState({}, '', window.location.href.split('#')[0])
fetch(`api/get/${this.secretId}`)
.then(resp => {
@ -89,11 +119,19 @@ export default {
appCrypto.dec(secret, this.securePassword)
.then(secret => {
this.secret = secret
})
.catch(() => {
this.$emit('error', this.$t('alert-something-went-wrong'))
const meta = new OTSMeta(secret)
this.secret = meta.secret
meta.files.forEach(file => {
file.arrayBuffer()
.then(ab => {
const blobURL = window.URL.createObjectURL(new Blob([ab], { type: file.type }))
this.files.push({ name: file.name, size: ab.byteLength, url: blobURL })
})
})
this.secretLoading = false
})
.catch(() => this.$emit('error', this.$t('alert-something-went-wrong')))
})
})
.catch(() => {