diff --git a/components/cryptForm.js b/components/cryptForm.js index 6ecee91..297da63 100644 --- a/components/cryptForm.js +++ b/components/cryptForm.js @@ -2,6 +2,7 @@ import { Box, Button, Text, + Link, Checkbox, useToast, Modal, @@ -16,6 +17,7 @@ import { import { FaDownload, FaEdit } from 'react-icons/fa'; import { useRef, useState } from 'react'; import dynamic from 'next/dynamic'; +import NextLink from 'next/link'; import crypto from 'crypto'; const Editor = dynamic(() => import('./editor'), { ssr: false }); @@ -24,6 +26,15 @@ function isGzip(data) { return data[0] == 0x1F && data[1] == 0x8B; } +function isJSON(data) { + try { + JSON.parse(data.toString()); + } catch (e) { + return false; + } + return true; +} + async function pipeThrough(data, stream) { let piped = Buffer.from(''); const reader = new Blob([data]).stream() @@ -41,10 +52,38 @@ async function pipeThrough(data, stream) { return piped; } +async function cryptData(data, password, isEncryption, shouldGzip) { + let wasGunzipped = false; + if (isEncryption) { + if (shouldGzip) + data = await pipeThrough(data, new CompressionStream('gzip')); + + if (password) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-128-cbc', crypto.pbkdf2Sync(password, iv, 100, 16, 'sha1'), iv); + data = Buffer.concat([iv, cipher.update(data), cipher.final()]); + } + } else { + if (password) { + const iv = data.subarray(0, 16); + const decipher = crypto.createDecipheriv('aes-128-cbc', crypto.pbkdf2Sync(password, iv, 100, 16, 'sha1'), iv); + data = Buffer.concat([decipher.update(data.subarray(16)), decipher.final()]); + } + + if (isGzip(data)) { + wasGunzipped = true; + data = await pipeThrough(data, new DecompressionStream('gzip')); + } + } + + return { wasGunzipped, cryptedData: data }; +} + export default function CryptForm({ isEncryption, isLoading, setIsLoading, password }) { const toast = useToast(); const saveFileRef = useRef(); const [data, setData] = useState(null); + const [editorData, setEditorData] = useState(null); const [shouldGzip, setShouldGzip] = useState(false); const [isEncryptionWarning, setIsEncryptionWarning] = useState(false); const { isOpen, onOpen: _onOpen, onClose: _onClose } = useDisclosure(); @@ -62,6 +101,13 @@ export default function CryptForm({ isEncryption, isLoading, setIsLoading, passw setIsEncryptionWarning(false); }; + const setDownloadData = (data, fileName) => { + const blobUrl = window.URL.createObjectURL(new Blob([data], { type: 'binary/octet-stream' })); + const downloader = document.getElementById('downloader'); + downloader.href = blobUrl; + downloader.download = fileName; + }; + const download = () => { setData(null); saveFileRef.current.value = ''; @@ -79,8 +125,10 @@ export default function CryptForm({ isEncryption, isLoading, setIsLoading, passw ref={saveFileRef} disabled={isLoading} onChange={changeEvent => { - if (!changeEvent.target.files.length) + if (!changeEvent.target.files.length) { + setData(null); return; + } const fileReader = new FileReader(); fileReader.onload = loadEvent => setData(Buffer.from(loadEvent.target.result)); @@ -117,7 +165,74 @@ export default function CryptForm({ isEncryption, isLoading, setIsLoading, passw
{!isEncryption && ( - } colorScheme='teal' width='100%' mt='2' display='block' onClick={onEditorOpen}>Open editor + } + colorScheme='orange' + width='100%' mt='2' + display='block' + onClick={async () => { + if (!data || (!password && !isGzip(data) && !isJSON(data))) { + toast({ + title: `Failed ${isEncryption ? 'encrypting' : 'decrypting'} the save file`, + description: !data ? 'No file chosen' : 'No password provided', + status: 'error', + duration: 2000, + isClosable: true, + position: 'bottom-left' + }); + + return; + } + + setIsLoading(true); + + let decryptedData; + try { + decryptedData = await cryptData(data, password, false); + } catch (e) { + console.error(e); + toast({ + title: 'Failed decrypting the save file', + description: 'Wrong decryption password? Try leaving the password field empty.', + status: 'error', + duration: 3500, + isClosable: true, + position: 'bottom-left' + }); + + setIsLoading(false); + return; + } + + if (!isJSON(decryptedData.cryptedData)) { + toast({ + title: 'Can\'t open editor', + description: ( + <> +