137 lines
5.0 KiB
TypeScript
137 lines
5.0 KiB
TypeScript
import { setTitle } from '../../functions/Route'
|
|
import HeaderMessage from '../layout/HeaderMessage'
|
|
import InputButton from '../inputs/Button'
|
|
import InputButtonRow from '../inputs/ButtonRow'
|
|
import InputLabel from '../inputs/Label'
|
|
import InputDescription from '../inputs/Description'
|
|
import './styles/Settings.css'
|
|
|
|
export default function ViewSettings() {
|
|
const GIFUU_PREFIX = '_|GIFUU_BACKUP:DO_NOT_SHARE|_'
|
|
|
|
async function onDataBackup() {
|
|
const password = prompt('Please enter a password for this backup (Leave empty for none):')
|
|
if (password === null) return
|
|
|
|
// Generate Key
|
|
const enc = new TextEncoder()
|
|
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'])
|
|
const salt = crypto.getRandomValues(new Uint8Array(16))
|
|
const key = await crypto.subtle.deriveKey(
|
|
{ name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['encrypt'],
|
|
)
|
|
|
|
// Encrypt String
|
|
const data = JSON.stringify(localStorage)
|
|
const nonce = crypto.getRandomValues(new Uint8Array(12))
|
|
const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, key, enc.encode(data))
|
|
|
|
// Package Contents
|
|
const bundle = new Uint8Array(salt.byteLength + nonce.byteLength + cipher.byteLength)
|
|
bundle.set(salt, 0)
|
|
bundle.set(nonce, 16)
|
|
bundle.set(new Uint8Array(cipher), 28)
|
|
|
|
const string = GIFUU_PREFIX + btoa(String.fromCharCode(...bundle))
|
|
navigator.clipboard.writeText(string)
|
|
|
|
alert('Data has been copied to clipboard.')
|
|
}
|
|
|
|
async function onDataRestore() {
|
|
// Retrieve User Data
|
|
const blob = await navigator.clipboard.readText()
|
|
if (!blob.startsWith(GIFUU_PREFIX)) {
|
|
alert('Contents stored in clipboard is not a backup.')
|
|
return
|
|
}
|
|
const password = prompt('Please enter the password for this backup:')
|
|
if (password === null) return
|
|
|
|
// Unpackage Contents
|
|
const bundle = Uint8Array.from(atob(blob.slice(GIFUU_PREFIX.length)), (c) => c.charCodeAt(0))
|
|
const salt = bundle.slice(0, 16)
|
|
const iv = bundle.slice(16, 28)
|
|
const cipher = bundle.slice(28)
|
|
|
|
// Generate Key
|
|
const enc = new TextEncoder()
|
|
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey'])
|
|
const key = await crypto.subtle.deriveKey(
|
|
{ name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['decrypt'],
|
|
)
|
|
|
|
// Decrypt String
|
|
let raw: string
|
|
try {
|
|
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, cipher)
|
|
raw = new TextDecoder().decode(plain)
|
|
} catch (err) {
|
|
console.error('Backup Error:', err)
|
|
alert('Incorrect Password')
|
|
return
|
|
}
|
|
|
|
// Parse String
|
|
try {
|
|
const data = JSON.parse(raw)
|
|
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
throw 'Invalid Object'
|
|
}
|
|
Object.entries(data).forEach(([k, v]) => {
|
|
if (typeof k === 'string' && typeof v === 'string') {
|
|
localStorage.setItem(k, v)
|
|
}
|
|
})
|
|
} catch (err) {
|
|
console.error('Backup Error:', err)
|
|
alert('Invalid data found in backup.')
|
|
}
|
|
|
|
alert('Data restored, the webpage will now refresh.')
|
|
window.location.reload()
|
|
}
|
|
|
|
setTitle('Settings')
|
|
return (
|
|
<>
|
|
<HeaderMessage label="View: Settings" />
|
|
<div className="view-settings">
|
|
<div className="section">
|
|
<InputLabel for="" label="Data Management" />
|
|
<InputDescription>
|
|
Export your local preferences and tokens for transfer or backup purposes to your devices
|
|
clipboard. Do not share these with anybody!
|
|
</InputDescription>
|
|
<InputButtonRow split={true}>
|
|
<InputButton
|
|
id="action-backup"
|
|
label="Backup"
|
|
onClick={onDataBackup}
|
|
disabled={false}
|
|
rainbow={false}
|
|
selected={false}
|
|
/>
|
|
<InputButton
|
|
id="action-restore"
|
|
label="Restore"
|
|
onClick={onDataRestore}
|
|
disabled={false}
|
|
rainbow={false}
|
|
selected={false}
|
|
/>
|
|
</InputButtonRow>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|