242 lines
6.4 KiB
JavaScript
242 lines
6.4 KiB
JavaScript
import 'preact/devtools'
|
|
import { h, Component } from 'preact'
|
|
import htm from 'htm'
|
|
import Crypto from 'crypto'
|
|
const html = htm.bind(h)
|
|
|
|
export class Keys extends Component {
|
|
constructor(props) {//{{{
|
|
super(props)
|
|
this.state = {
|
|
create: false,
|
|
}
|
|
|
|
props.nodeui.retrieveKeys()
|
|
}//}}}
|
|
render({ nodeui }, { create }) {//{{{
|
|
let keys = nodeui.keys.value
|
|
.sort((a,b)=>{
|
|
if(a.description < b.description) return -1
|
|
if(a.description > b.description) return 1
|
|
return 0
|
|
})
|
|
.map(key=>
|
|
html`<${KeyComponent} key=${`key-${key.ID}`} model=${key} />`
|
|
)
|
|
|
|
let createButton = ''
|
|
let createComponents = ''
|
|
if(create) {
|
|
createComponents = html`
|
|
<div id="key-create">
|
|
<h2>New key</h2>
|
|
|
|
<div class="fields">
|
|
<input type="text" id="key-description" placeholder="Name" />
|
|
|
|
<input type="password" id="key-pass1" placeholder="Password" />
|
|
<input type="password" id="key-pass2" placeholder="Repeat password" />
|
|
|
|
<textarea id="key-key" placeholder="Key"></textarea>
|
|
|
|
<div>
|
|
<button class="generate" onclick=${()=>this.generateKey()}>Generate</button>
|
|
<button class="create" onclick=${()=>this.createKey()}>Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
} else {
|
|
createButton = html`<div style="margin-top: 16px;"><button onclick=${()=>this.setState({ create: true })}>Create new key</button></div>`
|
|
}
|
|
|
|
return html`
|
|
<div id="keys">
|
|
<h1>Encryption keys</h1>
|
|
<p>
|
|
Unlock a key by clicking its name. Lock it by clicking it again.
|
|
</p>
|
|
|
|
<p>
|
|
Copy the key and store it in a very secure place to have a way to access notes
|
|
in case the password is forgotten, or database is corrupted.
|
|
</p>
|
|
|
|
<p>Click "View key" after unlocking it.</p>
|
|
|
|
${createButton}
|
|
${createComponents}
|
|
|
|
<h2>Keys</h2>
|
|
<div class="key-list">
|
|
${keys}
|
|
</div>
|
|
</div>`
|
|
}//}}}
|
|
|
|
generateKey() {//{{{
|
|
let keyTextarea = document.getElementById('key-key')
|
|
let key = sjcl.codec.hex.fromBits(Crypto.generate_key()).replace(/(....)/g, '$1 ').trim()
|
|
keyTextarea.value = key
|
|
}//}}}
|
|
validateNewKey() {//{{{
|
|
let keyDescription = document.getElementById('key-description').value
|
|
let keyTextarea = document.getElementById('key-key').value
|
|
let pass1 = document.getElementById('key-pass1').value
|
|
let pass2 = document.getElementById('key-pass2').value
|
|
|
|
if(keyDescription.trim() == '')
|
|
throw new Error('The key has to have a description')
|
|
|
|
if(pass1.trim() == '' || pass1.length < 4)
|
|
throw new Error('The password has to be at least 4 characters long.')
|
|
|
|
if(pass1 != pass2)
|
|
throw new Error(`Passwords doesn't match`)
|
|
|
|
let cleanKey = keyTextarea.replace(/\s+/g, '')
|
|
if(!cleanKey.match(/^[0-9a-f]{64}$/i))
|
|
throw new Error('Invalid key - has to be 64 characters of 0-9 and A-F')
|
|
}//}}}
|
|
createKey() {//{{{
|
|
try {
|
|
this.validateNewKey()
|
|
|
|
let description = document.getElementById('key-description').value
|
|
let keyAscii = document.getElementById('key-key').value
|
|
let pass1 = document.getElementById('key-pass1').value
|
|
|
|
// Key in hex taken from user.
|
|
let actual_key = sjcl.codec.hex.toBits(keyAscii.replace(/\s+/g, ''))
|
|
|
|
// Key generated from password, used to encrypt the actual key.
|
|
let pass_gen = Crypto.pass_to_key(pass1)
|
|
|
|
let crypto = new Crypto(pass_gen.key)
|
|
let encrypted_actual_key = crypto.encrypt(actual_key, 0x1n, false)
|
|
|
|
// Database value is salt + actual key, needed to generate the same key from the password.
|
|
let db_encoded = sjcl.codec.hex.fromBits(
|
|
pass_gen.salt.concat(encrypted_actual_key)
|
|
)
|
|
|
|
// Create on server.
|
|
window._app.current.request('/key/create', {
|
|
description,
|
|
key: db_encoded,
|
|
})
|
|
.then(res=>{
|
|
let key = new Key(res.Key, this.props.nodeui.keyCounter)
|
|
this.props.nodeui.keys.value = this.props.nodeui.keys.value.concat(key)
|
|
})
|
|
.catch(window._app.current.responseError)
|
|
} catch(err) {
|
|
alert(err.message)
|
|
return
|
|
}
|
|
}//}}}
|
|
}
|
|
|
|
export class Key {
|
|
constructor(data, counter_callback) {//{{{
|
|
this.ID = data.ID
|
|
this.description = data.Description
|
|
this.encryptedKey = data.Key
|
|
this.key = null
|
|
|
|
this._counter_cbk = counter_callback
|
|
|
|
let hex_key = window.sessionStorage.getItem(`key-${this.ID}`)
|
|
if(hex_key)
|
|
this.key = sjcl.codec.hex.toBits(hex_key)
|
|
}//}}}
|
|
status() {//{{{
|
|
if(this.key === null)
|
|
return 'locked'
|
|
return 'unlocked'
|
|
}//}}}
|
|
lock() {//{{{
|
|
this.key = null
|
|
window.sessionStorage.removeItem(`key-${this.ID}`)
|
|
}//}}}
|
|
unlock(password) {//{{{
|
|
let db = sjcl.codec.hex.toBits(this.encryptedKey)
|
|
let salt = db.slice(0, 4)
|
|
let pass_key = Crypto.pass_to_key(password, salt)
|
|
let crypto = new Crypto(pass_key.key)
|
|
this.key = crypto.decrypt(sjcl.codec.base64.fromBits(db.slice(4)))
|
|
window.sessionStorage.setItem(`key-${this.ID}`, sjcl.codec.hex.fromBits(this.key))
|
|
}//}}}
|
|
async counter() {//{{{
|
|
return this._counter_cbk()
|
|
}//}}}
|
|
}
|
|
|
|
export class KeyComponent extends Component {
|
|
constructor({ model }) {//{{{
|
|
super({ model })
|
|
this.state = {
|
|
show_key: false,
|
|
}
|
|
}//}}}
|
|
render({ model }, { show_key }) {//{{{
|
|
let status = ''
|
|
switch(model.status()) {
|
|
case 'locked':
|
|
status = html`<div class="status locked"><img src="/images/${window._VERSION}/padlock-closed.svg" /></div>`
|
|
break
|
|
|
|
case 'unlocked':
|
|
status = html`<div class="status unlocked"><img src="/images/${window._VERSION}/padlock-open.svg" /></div>`
|
|
break
|
|
}
|
|
|
|
let hex_key = ''
|
|
if(show_key) {
|
|
if(model.status() == 'locked')
|
|
hex_key = html`<div class="hex-key">Unlock key first</div>`
|
|
else {
|
|
let key = sjcl.codec.hex.fromBits(model.key)
|
|
key = key.replace(/(....)/g, "$1 ").trim()
|
|
hex_key = html`<div class="hex-key">${key}</div>`
|
|
}
|
|
}
|
|
|
|
let unlocked = model.status()=='unlocked'
|
|
|
|
return html`
|
|
<div class="status" onclick=${()=>this.toggle()}>${status}</div>
|
|
<div class="description" onclick=${()=>this.toggle()}>${model.description}</div>
|
|
<div class="view" onclick=${()=>this.toggleViewKey()}>${unlocked ? 'View key' : ''}</div>
|
|
${hex_key}
|
|
`
|
|
}//}}}
|
|
toggle() {//{{{
|
|
if(this.props.model.status() == 'locked')
|
|
this.unlock()
|
|
else
|
|
this.lock()
|
|
}//}}}
|
|
lock() {//{{{
|
|
this.props.model.lock()
|
|
this.forceUpdate()
|
|
}//}}}
|
|
unlock() {//{{{
|
|
let pass = prompt("Password")
|
|
if(!pass)
|
|
return
|
|
|
|
try {
|
|
this.props.model.unlock(pass)
|
|
this.forceUpdate()
|
|
} catch(err) {
|
|
alert(err)
|
|
}
|
|
}//}}}
|
|
toggleViewKey() {//{{{
|
|
this.setState({ show_key: !this.state.show_key })
|
|
}//}}}
|
|
}
|
|
|
|
// vim: foldmethod=marker
|