rc-1
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user