Keys unlocking/locking
This commit is contained in:
parent
f9dfc8835c
commit
c65f46a17d
13 changed files with 3271 additions and 82 deletions
69
static/js/crypto.mjs
Normal file
69
static/js/crypto.mjs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
export default class Crypto {
|
||||
constructor(key) {
|
||||
if(typeof key === 'string')
|
||||
this.key = sjcl.codec.base64.toBits(base64_key)
|
||||
else
|
||||
this.key = key
|
||||
|
||||
this.aes = new sjcl.cipher.aes(this.key)
|
||||
}
|
||||
|
||||
static generate_key() {
|
||||
return sjcl.random.randomWords(8)
|
||||
}
|
||||
|
||||
static pass_to_key(pass, salt = null) {
|
||||
if(salt === null)
|
||||
salt = sjcl.random.randomWords(4) // 128 bits (16 bytes)
|
||||
let key = sjcl.misc.pbkdf2(pass, salt, 10000)
|
||||
|
||||
return {
|
||||
salt,
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
encrypt(plaintext_data_in_bits, counter, return_encoded = true) {
|
||||
// 8 bytes of random data, (1 word = 4 bytes) * 2
|
||||
// with 8 bytes of byte encoded counter is used as
|
||||
// IV to guarantee a non-repeated IV (which is a catastrophe).
|
||||
// Assumes counter value is kept unique. Counter is taken from
|
||||
// Postgres sequence.
|
||||
let random_bits = sjcl.random.randomWords(2)
|
||||
let iv_bytes = sjcl.codec.bytes.fromBits(random_bits)
|
||||
for (let i = 0; i < 8; ++i) {
|
||||
let mask = 0xffn << BigInt(i*8)
|
||||
let counter_i_byte = (counter & mask) >> BigInt(i*8)
|
||||
iv_bytes[15-i] = Number(counter_i_byte)
|
||||
}
|
||||
let iv = sjcl.codec.bytes.toBits(iv_bytes)
|
||||
|
||||
let encrypted = sjcl.mode['ccm'].encrypt(
|
||||
this.aes,
|
||||
plaintext_data_in_bits,
|
||||
iv,
|
||||
)
|
||||
|
||||
// Returning 16 bytes (4 words) IV + encrypted data.
|
||||
if(return_encoded)
|
||||
return sjcl.codec.base64.fromBits(
|
||||
iv.concat(encrypted)
|
||||
)
|
||||
else
|
||||
return iv.concat(encrypted)
|
||||
}
|
||||
|
||||
decrypt(encrypted_base64_data) {
|
||||
try {
|
||||
let encoded = sjcl.codec.base64.toBits(encrypted_base64_data)
|
||||
let iv = encoded.slice(0, 4) // in words (4 bytes), not bytes
|
||||
let encrypted_data = encoded.slice(4)
|
||||
return sjcl.mode['ccm'].decrypt(this.aes, encrypted_data, iv)
|
||||
} catch(err) {
|
||||
if(err.message == `ccm: tag doesn't match`)
|
||||
throw('Decryption failed')
|
||||
else
|
||||
throw(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
113
static/js/key.mjs
Normal file
113
static/js/key.mjs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
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.retrieveKeys()
|
||||
}//}}}
|
||||
render({ nodeui }) {//{{{
|
||||
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} />`
|
||||
)
|
||||
|
||||
return html`
|
||||
<div id="keys">
|
||||
<h1>Keys</h1>
|
||||
<div class="key-list">
|
||||
${keys}
|
||||
</div>
|
||||
</div>`
|
||||
}//}}}
|
||||
|
||||
retrieveKeys() {//{{{
|
||||
window._app.current.request('/key/retrieve', {})
|
||||
.then(res=>{
|
||||
this.props.nodeui.keys.value = res.Keys.map(keyData=>new Key(keyData))
|
||||
})
|
||||
.catch(window._app.current.responseError)
|
||||
}//}}}
|
||||
}
|
||||
|
||||
export class Key {
|
||||
constructor(data) {//{{{
|
||||
this.ID = data.ID
|
||||
this.description = data.Description
|
||||
this.unlockedKey = data.Key
|
||||
this.key = null
|
||||
|
||||
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.unlockedKey)
|
||||
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))
|
||||
}//}}}
|
||||
}
|
||||
|
||||
export class KeyComponent extends Component {
|
||||
render({ model }) {//{{{
|
||||
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
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="status" onclick=${()=>this.toggle()}>${status}</div>
|
||||
<div class="description" onclick=${()=>this.toggle()}>${model.description}</div>
|
||||
`
|
||||
}//}}}
|
||||
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)
|
||||
}
|
||||
}//}}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
2574
static/js/lib/sjcl.js
Normal file
2574
static/js/lib/sjcl.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
import { h, Component, createRef } from 'preact'
|
||||
import htm from 'htm'
|
||||
import { signal } from 'preact/signals'
|
||||
import { Keys, Key } from 'key'
|
||||
const html = htm.bind(h)
|
||||
|
||||
export class NodeUI extends Component {
|
||||
|
|
@ -9,8 +10,9 @@ export class NodeUI extends Component {
|
|||
this.menu = signal(false)
|
||||
this.node = signal(null)
|
||||
this.nodeContent = createRef()
|
||||
this.upload = signal(false)
|
||||
this.keys = signal([])
|
||||
|
||||
this.page = signal('node')
|
||||
window.addEventListener('popstate', evt=>{
|
||||
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
||||
this.goToNode(evt.state.nodeID, true)
|
||||
|
|
@ -35,8 +37,8 @@ export class NodeUI extends Component {
|
|||
).reverse())
|
||||
|
||||
let children = node.Children.sort((a,b)=>{
|
||||
if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1;
|
||||
if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1;
|
||||
if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1
|
||||
if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1
|
||||
return 0
|
||||
}).map(child=>html`
|
||||
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
||||
|
|
@ -44,23 +46,46 @@ export class NodeUI extends Component {
|
|||
|
||||
let modified = ''
|
||||
if(this.props.app.nodeModified.value)
|
||||
modified = 'modified';
|
||||
modified = 'modified'
|
||||
|
||||
let upload = '';
|
||||
if(this.upload.value)
|
||||
upload = html`<${UploadUI} nodeui=${this} />`
|
||||
|
||||
let menu = '';
|
||||
// Page to display
|
||||
let page = ''
|
||||
switch(this.page.value) {
|
||||
case 'node':
|
||||
if(node.ID > 0)
|
||||
page = html`
|
||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||
<div class="node-name">${node.Name}</div>
|
||||
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
||||
<${NodeFiles} node=${this.node.value} />
|
||||
`
|
||||
break
|
||||
|
||||
case 'upload':
|
||||
page = html`<${UploadUI} nodeui=${this} />`
|
||||
break
|
||||
|
||||
case 'node-properties':
|
||||
page = html`<${NodeProperties} nodeui=${this} />`
|
||||
break
|
||||
|
||||
case 'keys':
|
||||
page = html`<${Keys} nodeui=${this} />`
|
||||
break
|
||||
}
|
||||
|
||||
let menu = ''
|
||||
if(this.menu.value)
|
||||
upload = html`<${Menu} nodeui=${this} />`
|
||||
menu = html`<${Menu} nodeui=${this} />`
|
||||
|
||||
return html`
|
||||
${menu}
|
||||
${upload}
|
||||
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
||||
<div class="tree"><img src="/images/${window._VERSION}/tree.svg" onclick=${()=>document.getElementById('app').classList.toggle('toggle-tree')} /></div>
|
||||
<div class="name">Notes</div>
|
||||
<div class="add" onclick=${evt=>this.createNode(evt)}>+</div>
|
||||
<div class="keys" onclick=${()=>this.showPage('keys')}><img src="/images/${window._VERSION}/padlock.svg" /></div>
|
||||
<div class="menu" onclick=${evt=>this.showMenu(evt)}>☰</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -68,14 +93,7 @@ export class NodeUI extends Component {
|
|||
<div class="crumbs">${crumbs}</crumbs>
|
||||
</div>
|
||||
|
||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||
|
||||
${node.ID > 0 ? html`
|
||||
<div class="node-name">${node.Name}</div>
|
||||
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
||||
` : html``}
|
||||
|
||||
<${NodeFiles} node=${this.node.value} />
|
||||
${page}
|
||||
`
|
||||
}//}}}
|
||||
componentDidMount() {//{{{
|
||||
|
|
@ -91,6 +109,27 @@ export class NodeUI extends Component {
|
|||
keyHandler(evt) {//{{{
|
||||
let handled = true
|
||||
switch(evt.key.toUpperCase()) {
|
||||
case 'E':
|
||||
if(evt.shiftKey && evt.altKey)
|
||||
this.showPage('keys')
|
||||
else
|
||||
handled = false
|
||||
break
|
||||
|
||||
case 'N':
|
||||
if(evt.shiftKey && evt.altKey)
|
||||
this.createNode()
|
||||
else
|
||||
handled = false
|
||||
break
|
||||
|
||||
case 'P':
|
||||
if(evt.shiftKey && evt.altKey)
|
||||
this.showPage('node-properties')
|
||||
else
|
||||
handled = false
|
||||
break
|
||||
|
||||
case 'S':
|
||||
if(evt.ctrlKey || (evt.shiftKey && evt.altKey))
|
||||
this.saveNode()
|
||||
|
|
@ -98,16 +137,10 @@ export class NodeUI extends Component {
|
|||
handled = false
|
||||
break
|
||||
|
||||
case 'N':
|
||||
if((evt.ctrlKey && evt.AltKey) || (evt.shiftKey && evt.altKey))
|
||||
this.createNode()
|
||||
else
|
||||
handled = false
|
||||
break
|
||||
|
||||
case 'U':
|
||||
if((evt.ctrlKey && evt.altKey) || (evt.shiftKey && evt.altKey))
|
||||
this.upload.value = true
|
||||
if(evt.shiftKey && evt.altKey)
|
||||
this.showPage('upload')
|
||||
else
|
||||
handled = false
|
||||
|
||||
|
|
@ -151,7 +184,7 @@ export class NodeUI extends Component {
|
|||
|
||||
// Hide tree toggle, as this would be the next natural action to do manually anyway.
|
||||
// At least in mobile mode.
|
||||
document.getElementById('app').classList.remove('toggle-tree');
|
||||
document.getElementById('app').classList.remove('toggle-tree')
|
||||
})
|
||||
}//}}}
|
||||
createNode(evt) {//{{{
|
||||
|
|
@ -184,6 +217,10 @@ export class NodeUI extends Component {
|
|||
this.menu.value = false
|
||||
})
|
||||
}//}}}
|
||||
|
||||
showPage(pg) {//{{{
|
||||
this.page.value = pg
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class NodeContent extends Component {
|
||||
|
|
@ -342,12 +379,12 @@ export class Node {
|
|||
})
|
||||
.then(blob=>{
|
||||
let url = window.URL.createObjectURL(blob)
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fname;
|
||||
document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
|
||||
a.click();
|
||||
a.remove(); //afterwards we remove the element again
|
||||
let a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fname
|
||||
document.body.appendChild(a) // we need to append the element to the dom -> otherwise it will not work in firefox
|
||||
a.click()
|
||||
a.remove() //afterwards we remove the element again
|
||||
})
|
||||
}//}}}
|
||||
}
|
||||
|
|
@ -358,7 +395,8 @@ class Menu extends Component {
|
|||
<div id="blackout" onclick=${()=>nodeui.menu.value = false}></div>
|
||||
<div id="menu">
|
||||
<div class="item" onclick=${()=>{ nodeui.renameNode(); nodeui.menu.value = false }}>Rename</div>
|
||||
<div class="item separator" onclick=${()=>{ nodeui.deleteNode(); nodeui.menu.value = false }}>Delete</div>
|
||||
<div class="item" onclick=${()=>{ nodeui.deleteNode(); nodeui.menu.value = false }}>Delete</div>
|
||||
<div class="item separator" onclick=${()=>{ nodeui.showPage('properties'); nodeui.menu.value = false }}>Properties</div>
|
||||
<div class="item separator" onclick=${()=>{ nodeui.upload.value = true; nodeui.menu.value = false }}>Upload</div>
|
||||
<div class="item" onclick=${()=>{ nodeui.logout(); nodeui.menu.value = false }}>Log out</div>
|
||||
</div>
|
||||
|
|
@ -463,6 +501,19 @@ class UploadUI extends Component {
|
|||
request.send(formdata)
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class NodeProperties extends Component {
|
||||
constructor(props) {//{{{
|
||||
super(props)
|
||||
}//}}}
|
||||
render({ nodeui }) {//{{{
|
||||
return html`
|
||||
<div id="blackout" onclick=${()=>nodeui.properties.value = false}></div>
|
||||
<div id="properties">
|
||||
<h1>Node properties</h1>
|
||||
<input type="checkbox" id="node-encrypted" /> <label for="node-encrypted">Encrypted</label>
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue