Keys unlocking/locking

This commit is contained in:
Magnus Åhall 2023-07-01 20:33:26 +02:00
parent f9dfc8835c
commit c65f46a17d
13 changed files with 3271 additions and 82 deletions

69
static/js/crypto.mjs Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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