use base64 instead of hex and refactor a bit
This commit is contained in:
parent
15db684a93
commit
f60951e7d5
|
@ -0,0 +1,61 @@
|
||||||
|
import type { EncryptedFileDTO, FileDTO } from './api'
|
||||||
|
import { Crypto } from './crypto'
|
||||||
|
|
||||||
|
abstract class CryptAdapter<T> {
|
||||||
|
abstract encrypt(plaintext: T, key: CryptoKey): Promise<string>
|
||||||
|
abstract decrypt(ciphertext: string, key: CryptoKey): Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptTextAdapter implements CryptAdapter<string> {
|
||||||
|
async encrypt(plaintext: string, key: CryptoKey) {
|
||||||
|
return await Crypto.encrypt(new TextEncoder().encode(plaintext), key)
|
||||||
|
}
|
||||||
|
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||||
|
const plaintext = await Crypto.decrypt(ciphertext, key)
|
||||||
|
return new TextDecoder().decode(plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptBlobAdapter implements CryptAdapter<Blob> {
|
||||||
|
async encrypt(plaintext: Blob, key: CryptoKey) {
|
||||||
|
return await Crypto.encrypt(await plaintext.arrayBuffer(), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||||
|
const plaintext = await Crypto.decrypt(ciphertext, key)
|
||||||
|
return new Blob([plaintext], { type: 'application/octet-stream' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
|
||||||
|
async encrypt(plaintext: FileDTO[], key: CryptoKey) {
|
||||||
|
const adapter = new CryptBlobAdapter()
|
||||||
|
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: await adapter.encrypt(file.contents, key),
|
||||||
|
}))
|
||||||
|
return JSON.stringify(await Promise.all(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||||
|
const adapter = new CryptBlobAdapter()
|
||||||
|
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
|
||||||
|
const files: FileDTO[] = await Promise.all(
|
||||||
|
data.map(async (file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: await adapter.decrypt(file.contents, key),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Adapters = {
|
||||||
|
Text: new CryptTextAdapter(),
|
||||||
|
Blob: new CryptBlobAdapter(),
|
||||||
|
Files: new CryptFilesAdapter(),
|
||||||
|
}
|
|
@ -11,6 +11,10 @@ export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
||||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||||
|
|
||||||
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
||||||
|
contents: Blob
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||||
contents: string
|
contents: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,53 +19,79 @@ export class Hex {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALG = 'AES-GCM'
|
export class ArrayBufferUtils {
|
||||||
|
static async toString(buffer: ArrayBuffer): Promise<string> {
|
||||||
|
const reader = new window.FileReader()
|
||||||
|
reader.readAsDataURL(new Blob([buffer]))
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
reader.onloadend = () => resolve(reader.result as string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function getRandomBytes(size = 16): Uint8Array {
|
static async fromString(s: string): Promise<ArrayBuffer> {
|
||||||
return window.crypto.getRandomValues(new Uint8Array(size))
|
return fetch(s)
|
||||||
|
.then((r) => r.blob())
|
||||||
|
.then((b) => b.arrayBuffer())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getKeyFromString(password: string) {
|
export class Crypto {
|
||||||
return window.crypto.subtle.importKey(
|
private static ALG = 'AES-GCM'
|
||||||
'raw',
|
private static DELIMITER = ':::'
|
||||||
new TextEncoder().encode(password),
|
|
||||||
'PBKDF2',
|
|
||||||
false,
|
|
||||||
['deriveBits', 'deriveKey']
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
|
public static getRandomBytes(size: number): Uint8Array {
|
||||||
const iterations = 100_000
|
return window.crypto.getRandomValues(new Uint8Array(size))
|
||||||
return window.crypto.subtle.deriveKey(
|
}
|
||||||
{
|
|
||||||
name: 'PBKDF2',
|
|
||||||
salt,
|
|
||||||
iterations,
|
|
||||||
hash: 'SHA-512',
|
|
||||||
},
|
|
||||||
key,
|
|
||||||
{ name: ALG, length: 256 },
|
|
||||||
true,
|
|
||||||
['encrypt', 'decrypt']
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function encrypt(plaintext: string, key: CryptoKey) {
|
public static getKeyFromString(password: string) {
|
||||||
const salt = getRandomBytes(16)
|
return window.crypto.subtle.importKey(
|
||||||
const derived = await getDerivedForKey(key, salt)
|
'raw',
|
||||||
const iv = getRandomBytes(16)
|
new TextEncoder().encode(password),
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
'PBKDF2',
|
||||||
{ name: ALG, iv },
|
false,
|
||||||
derived,
|
['deriveBits', 'deriveKey']
|
||||||
new TextEncoder().encode(plaintext)
|
)
|
||||||
)
|
}
|
||||||
return [salt, iv, encrypted].map(Hex.encode).join(':')
|
public static async getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
|
||||||
}
|
const iterations = 100_000
|
||||||
|
return window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt,
|
||||||
|
iterations,
|
||||||
|
hash: 'SHA-512',
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
{ name: this.ALG, length: 256 },
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function decrypt(ciphertext: string, key: CryptoKey) {
|
public static async encrypt(plaintext: ArrayBuffer, key: CryptoKey): Promise<string> {
|
||||||
const [salt, iv, encrypted] = ciphertext.split(':').map(Hex.decode)
|
const salt = this.getRandomBytes(16)
|
||||||
const derived = await getDerivedForKey(key, salt)
|
const derived = await this.getDerivedForKey(key, salt)
|
||||||
const plaintext = await window.crypto.subtle.decrypt({ name: ALG, iv }, derived, encrypted)
|
const iv = this.getRandomBytes(16)
|
||||||
return new TextDecoder().decode(plaintext)
|
const encrypted: ArrayBuffer = await window.crypto.subtle.encrypt(
|
||||||
|
{ name: this.ALG, iv },
|
||||||
|
derived,
|
||||||
|
plaintext
|
||||||
|
)
|
||||||
|
const data = [
|
||||||
|
Hex.encode(salt),
|
||||||
|
Hex.encode(iv),
|
||||||
|
await ArrayBufferUtils.toString(encrypted),
|
||||||
|
].join(this.DELIMITER)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async decrypt(ciphertext: string, key: CryptoKey): Promise<ArrayBuffer> {
|
||||||
|
const splitted = ciphertext.split(this.DELIMITER)
|
||||||
|
const salt = Hex.decode(splitted[0])
|
||||||
|
const iv = Hex.decode(splitted[1])
|
||||||
|
const encrypted = await ArrayBufferUtils.fromString(splitted[2])
|
||||||
|
const derived = await this.getDerivedForKey(key, salt)
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt({ name: this.ALG, iv }, derived, encrypted)
|
||||||
|
return plaintext
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
export class Files {
|
|
||||||
static toString(f: File | Blob): Promise<string> {
|
|
||||||
const reader = new window.FileReader()
|
|
||||||
reader.readAsDataURL(f)
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
reader.onloadend = () => resolve(reader.result as string)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fromString(s: string): Promise<Blob> {
|
|
||||||
return fetch(s).then((r) => r.blob())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +1,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileDTO } from '$lib/api'
|
|
||||||
import { Files } from '$lib/files'
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import Button from './Button.svelte'
|
|
||||||
import MaxSize from './MaxSize.svelte'
|
import type { FileDTO } from '$lib/api'
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||||
|
|
||||||
export let label: string = ''
|
export let label: string = ''
|
||||||
let files: File[] = []
|
export let files: FileDTO[] = []
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ file: string }>()
|
function fileToDTO(file: File): FileDTO {
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: file,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onInput(e: Event) {
|
async function onInput(e: Event) {
|
||||||
const input = e.target as HTMLInputElement
|
const input = e.target as HTMLInputElement
|
||||||
if (input?.files?.length) {
|
if (input?.files?.length) {
|
||||||
files = [...files, ...Array.from(input.files)]
|
files = [...files, ...Array.from(input.files).map(fileToDTO)]
|
||||||
const data: FileDTO[] = await Promise.all(
|
|
||||||
files.map(async (file) => ({
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
contents: await Files.toString(file),
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
dispatch('file', JSON.stringify(data))
|
|
||||||
} else {
|
|
||||||
dispatch('file', '')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(e: Event) {
|
function clear(e: Event) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
files = []
|
files = []
|
||||||
dispatch('file', '')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -57,7 +51,9 @@
|
||||||
<div>
|
<div>
|
||||||
<b>{$t('file_upload.no_files_selected')}</b>
|
<b>{$t('file_upload.no_files_selected')}</b>
|
||||||
<br />
|
<br />
|
||||||
<small>{$t('common.max')}: <MaxSize /></small>
|
<small>
|
||||||
|
{$t('common.max')}: <MaxSize />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export type NoteResult = {
|
||||||
|
password: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
|
|
||||||
|
export let result: NoteResult
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
label={$t('common.share_link')}
|
||||||
|
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||||
|
copy
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{@html $t('home.new_note_notice')}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -1,20 +1,24 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileDTO, NotePublic } from '$lib/api'
|
|
||||||
import { Files } from '$lib/files'
|
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import Button from './Button.svelte'
|
|
||||||
|
|
||||||
export let note: NotePublic
|
import type { FileDTO, NotePublic } from '$lib/api'
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
|
||||||
|
export let note: DecryptedNote
|
||||||
|
|
||||||
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
||||||
let files: FileDTO[] = []
|
let files: FileDTO[] = []
|
||||||
|
|
||||||
$: if (note.meta.type === 'file') {
|
$: if (note.meta.type === 'file') {
|
||||||
files = JSON.parse(note.contents) as FileDTO[]
|
files = note.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
$: download = () => {
|
$: download = () => {
|
||||||
|
@ -24,7 +28,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(file: FileDTO) {
|
async function downloadFile(file: FileDTO) {
|
||||||
const f = new File([await Files.fromString(file.contents)], file.name, {
|
const f = new File([file.contents], file.name, {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
})
|
})
|
||||||
saveAs(f)
|
saveAs(f)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getRandomBytes, Hex } from '$lib/crypto'
|
|
||||||
import copyToClipboard from 'copy-to-clipboard'
|
import copyToClipboard from 'copy-to-clipboard'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import Icon from './Icon.svelte'
|
|
||||||
|
import { Crypto, Hex } from '$lib/crypto'
|
||||||
|
import Icon from '$lib/ui/Icon.svelte'
|
||||||
|
|
||||||
export let label: string = ''
|
export let label: string = ''
|
||||||
export let value: any
|
export let value: any
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
notify($t('home.copied_to_clipboard'))
|
notify($t('home.copied_to_clipboard'))
|
||||||
}
|
}
|
||||||
function randomFN() {
|
function randomFN() {
|
||||||
value = Hex.encode(getRandomBytes(20))
|
value = Hex.encode(Crypto.getRandomBytes(20))
|
||||||
}
|
}
|
||||||
|
|
||||||
function notify(msg: string, delay: number = 2000) {
|
function notify(msg: string, delay: number = 2000) {
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Note } from '$lib/api'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import { create,PayloadToLargeError } from '$lib/api'
|
import { blur } from 'svelte/transition'
|
||||||
import { encrypt,getKeyFromString,getRandomBytes,Hex } from '$lib/crypto'
|
|
||||||
|
import { Adapters } from '$lib/adapters'
|
||||||
|
import type { FileDTO, Note } from '$lib/api'
|
||||||
|
import { create, PayloadToLargeError } from '$lib/api'
|
||||||
|
import { Crypto, Hex } from '$lib/crypto'
|
||||||
import { status } from '$lib/stores/status'
|
import { status } from '$lib/stores/status'
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import Button from '$lib/ui/Button.svelte'
|
||||||
import FileUpload from '$lib/ui/FileUpload.svelte'
|
import FileUpload from '$lib/ui/FileUpload.svelte'
|
||||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||||
|
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||||
import Switch from '$lib/ui/Switch.svelte'
|
import Switch from '$lib/ui/Switch.svelte'
|
||||||
import TextArea from '$lib/ui/TextArea.svelte'
|
import TextArea from '$lib/ui/TextArea.svelte'
|
||||||
import TextInput from '$lib/ui/TextInput.svelte'
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
import { t } from 'svelte-intl-precompile'
|
|
||||||
import { blur } from 'svelte/transition'
|
|
||||||
|
|
||||||
let note: Note = {
|
let note: Note = {
|
||||||
contents: '',
|
contents: '',
|
||||||
|
@ -18,11 +21,12 @@
|
||||||
views: 1,
|
views: 1,
|
||||||
expiration: 60,
|
expiration: 60,
|
||||||
}
|
}
|
||||||
let result: { password: string; id: string } | null = null
|
let files: FileDTO[]
|
||||||
|
let result: NoteResult | null = null
|
||||||
let advanced = false
|
let advanced = false
|
||||||
let file = false
|
let isFile = false
|
||||||
let timeExpiration = false
|
let timeExpiration = false
|
||||||
let message = ''
|
let description = ''
|
||||||
let loading = false
|
let loading = false
|
||||||
let error: string | null = null
|
let error: string | null = null
|
||||||
|
|
||||||
|
@ -32,7 +36,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
message = $t('home.explanation', {
|
description = $t('home.explanation', {
|
||||||
values: {
|
values: {
|
||||||
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
||||||
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
||||||
|
@ -41,9 +45,9 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$: note.meta.type = file ? 'file' : 'text'
|
$: note.meta.type = isFile ? 'file' : 'text'
|
||||||
|
|
||||||
$: if (!file) {
|
$: if (!isFile) {
|
||||||
note.contents = ''
|
note.contents = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,13 +57,21 @@
|
||||||
try {
|
try {
|
||||||
error = null
|
error = null
|
||||||
loading = true
|
loading = true
|
||||||
const password = Hex.encode(getRandomBytes(32))
|
|
||||||
const key = await getKeyFromString(password)
|
const password = Hex.encode(Crypto.getRandomBytes(32))
|
||||||
if (note.contents === '') throw new EmptyContentError()
|
const key = await Crypto.getKeyFromString(password)
|
||||||
|
|
||||||
const data: Note = {
|
const data: Note = {
|
||||||
contents: await encrypt(note.contents, key),
|
contents: '',
|
||||||
meta: note.meta,
|
meta: note.meta,
|
||||||
}
|
}
|
||||||
|
if (isFile) {
|
||||||
|
if (files.length === 0) throw new EmptyContentError()
|
||||||
|
data.contents = await Adapters.Files.encrypt(files, key)
|
||||||
|
} else {
|
||||||
|
if (note.contents === '') throw new EmptyContentError()
|
||||||
|
data.contents = await Adapters.Text.encrypt(note.contents, key)
|
||||||
|
}
|
||||||
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
||||||
else data.views = parseInt(note.views as any)
|
else data.views = parseInt(note.views as any)
|
||||||
|
|
||||||
|
@ -74,46 +86,31 @@
|
||||||
} else if (e instanceof EmptyContentError) {
|
} else if (e instanceof EmptyContentError) {
|
||||||
error = $t('home.errors.empty_content')
|
error = $t('home.errors.empty_content')
|
||||||
} else {
|
} else {
|
||||||
|
console.error(e)
|
||||||
error = $t('home.errors.note_error')
|
error = $t('home.errors.note_error')
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if result}
|
{#if result}
|
||||||
<TextInput
|
<Result {result} />
|
||||||
type="text"
|
|
||||||
readonly
|
|
||||||
label={$t('common.share_link')}
|
|
||||||
value="{window.location.origin}/note/{result.id}#{result.password}"
|
|
||||||
copy
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{@html $t('home.new_note_notice')}
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<p>
|
<p>
|
||||||
{@html $status?.theme_text || $t('home.intro')}
|
{@html $status?.theme_text || $t('home.intro')}
|
||||||
</p>
|
</p>
|
||||||
<form on:submit|preventDefault={submit}>
|
<form on:submit|preventDefault={submit}>
|
||||||
<fieldset disabled={loading}>
|
<fieldset disabled={loading}>
|
||||||
{#if file}
|
{#if isFile}
|
||||||
<FileUpload label={$t('common.file')} on:file={(f) => (note.contents = f.detail)} />
|
<FileUpload label={$t('common.file')} bind:files />
|
||||||
{:else}
|
{:else}
|
||||||
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
|
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<Switch class="file" label={$t('common.file')} bind:value={file} />
|
<Switch class="file" label={$t('common.file')} bind:value={isFile} />
|
||||||
{#if $status?.allow_advanced}
|
{#if $status?.allow_advanced}
|
||||||
<Switch label={$t('common.advanced')} bind:value={advanced} />
|
<Switch label={$t('common.advanced')} bind:value={advanced} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -134,7 +131,7 @@
|
||||||
{#if loading}
|
{#if loading}
|
||||||
{$t('common.loading')}
|
{$t('common.loading')}
|
||||||
{:else}
|
{:else}
|
||||||
{message}
|
{description}
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { init, waitLocale, getLocaleFromNavigator } from 'svelte-intl-precompile'
|
import { getLocaleFromNavigator, init, waitLocale } from 'svelte-intl-precompile'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { registerAll } from '$locales'
|
import { registerAll } from '$locales'
|
||||||
registerAll()
|
registerAll()
|
||||||
|
|
|
@ -12,25 +12,25 @@
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import type { NotePublic } from '$lib/api'
|
import { Adapters } from '$lib/adapters'
|
||||||
import { get, info } from '$lib/api'
|
import { get, info } from '$lib/api'
|
||||||
import { decrypt, getKeyFromString } from '$lib/crypto'
|
import { Crypto } from '$lib/crypto'
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import Button from '$lib/ui/Button.svelte'
|
||||||
import ShowNote from '$lib/ui/ShowNote.svelte'
|
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||||
|
|
||||||
export let id: string
|
export let id: string
|
||||||
|
|
||||||
let password: string
|
let password: string
|
||||||
let note: NotePublic | null = null
|
let note: DecryptedNote | null = null
|
||||||
let exists = false
|
let exists = false
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let error = false
|
let error: string | null = null
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// Check if note exists
|
||||||
try {
|
try {
|
||||||
loading = true
|
loading = true
|
||||||
error = false
|
|
||||||
password = window.location.hash.slice(1)
|
password = window.location.hash.slice(1)
|
||||||
await info(id)
|
await info(id)
|
||||||
exists = true
|
exists = true
|
||||||
|
@ -41,16 +41,34 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actual contents of the note and decrypt it.
|
||||||
|
*/
|
||||||
async function show() {
|
async function show() {
|
||||||
try {
|
try {
|
||||||
error = false
|
error = null
|
||||||
loading = true
|
loading = true
|
||||||
const data = note || (await get(id)) // Don't get the content twice on wrong password.
|
const data = await get(id)
|
||||||
const key = await getKeyFromString(password)
|
const key = await Crypto.getKeyFromString(password)
|
||||||
data.contents = await decrypt(data.contents, key)
|
switch (data.meta.type) {
|
||||||
note = data
|
case 'text':
|
||||||
|
note = {
|
||||||
|
meta: { type: 'text' },
|
||||||
|
contents: await Adapters.Text.decrypt(data.contents, key),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'file':
|
||||||
|
note = {
|
||||||
|
meta: { type: 'file' },
|
||||||
|
contents: await Adapters.Files.decrypt(data.contents, key),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
error = $t('show.errors.unsupported_type')
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
error = true
|
error = $t('show.errors.decryption_failed')
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
@ -70,7 +88,7 @@
|
||||||
{#if error}
|
{#if error}
|
||||||
<br />
|
<br />
|
||||||
<p class="error-text">
|
<p class="error-text">
|
||||||
{$t('show.errors.decryption_failed')}
|
{error}
|
||||||
<br />
|
<br />
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
Loading…
Reference in New Issue