From 87a802e2106cfae892b93c936b21bc667101ebad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 12 Jul 2023 22:35:38 +0200 Subject: [PATCH] Work on cryptokeys --- key.go | 41 +++++- main.go | 61 +++++++- node.go | 41 ++++-- sql/0008.sql | 2 + sql/0009.sql | 1 + sql/0010.sql | 1 + static/css/main.css | 42 +++++- static/images/padlock-black.svg | 46 ++++++ static/js/crypto.mjs | 27 ++-- static/js/key.mjs | 160 ++++++++++++++++++--- static/js/node.mjs | 246 +++++++++++++++++++++++++++++--- static/less/main.less | 52 ++++++- 12 files changed, 637 insertions(+), 83 deletions(-) create mode 100644 sql/0008.sql create mode 100644 sql/0009.sql create mode 100644 sql/0010.sql create mode 100644 static/images/padlock-black.svg diff --git a/key.go b/key.go index fc3fe14..708fe39 100644 --- a/key.go +++ b/key.go @@ -14,7 +14,7 @@ type Key struct { Key string } -func (session Session) Keys() (keys []Key, err error) { +func (session Session) Keys() (keys []Key, err error) {// {{{ var rows *sqlx.Rows if rows, err = db.Queryx(`SELECT * FROM crypto_key WHERE user_id=$1`, session.UserID); err != nil { return @@ -31,4 +31,41 @@ func (session Session) Keys() (keys []Key, err error) { } return -} +}// }}} +func (session Session) KeyCreate(description, keyEncoded string) (key Key, err error) {// {{{ + var row *sqlx.Rows + if row, err = db.Queryx( + `INSERT INTO crypto_key(user_id, description, key) + VALUES($1, $2, $3) + RETURNING *`, + session.UserID, + description, + keyEncoded, + ); err != nil { + return + } + defer row.Close() + + key = Key{} + for row.Next() { + if err = row.StructScan(&key); err != nil { + return + } + } + + return +}// }}} +func (session Session) KeyCounter() (counter int64, err error) {// {{{ + var rows *sqlx.Rows + rows, err = db.Queryx(`SELECT nextval('aes_ccm_counter') AS counter`) + if err != nil { + return + } + defer rows.Close() + + rows.Next() + err = rows.Scan(&counter) + return +}// }}} + +// vim: foldmethod=marker diff --git a/main.go b/main.go index 1bc2819..3c0afb6 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ import ( const VERSION = "v0.2.2"; const LISTEN_HOST = "0.0.0.0"; -const DB_SCHEMA = 7 +const DB_SCHEMA = 10 var ( flagPort int @@ -76,7 +76,9 @@ func main() {// {{{ http.HandleFunc("/node/delete", nodeDelete) http.HandleFunc("/node/upload", nodeUpload) http.HandleFunc("/node/download", nodeDownload) - http.HandleFunc("/key/retrieve", keyRetrieve) + http.HandleFunc("/key/retrieve", keyRetrieve) + http.HandleFunc("/key/create", keyCreate) + http.HandleFunc("/key/counter", keyCounter) http.HandleFunc("/ws", websocketHandler) http.HandleFunc("/", staticHandler) @@ -288,13 +290,14 @@ func nodeUpdate(w http.ResponseWriter, r *http.Request) {// {{{ req := struct { NodeID int Content string + CryptoKeyID int }{} if err = parseRequest(r, &req); err != nil { responseError(w, err) return } - err = session.UpdateNode(req.NodeID, req.Content) + err = session.UpdateNode(req.NodeID, req.Content, req.CryptoKeyID) if err != nil { responseError(w, err) return @@ -562,6 +565,58 @@ func keyRetrieve(w http.ResponseWriter, r *http.Request) {// {{{ "Keys": keys, }) }// }}} +func keyCreate(w http.ResponseWriter, r *http.Request) {// {{{ + log.Println("/key/create") + var err error + var session Session + + if session, _, err = ValidateSession(r, true); err != nil { + responseError(w, err) + return + } + + req := struct { + Description string + Key string + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + key, err := session.KeyCreate(req.Description, req.Key) + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + "Key": key, + }) +}// }}} +func keyCounter(w http.ResponseWriter, r *http.Request) {// {{{ + log.Println("/key/counter") + var err error + var session Session + + if session, _, err = ValidateSession(r, true); err != nil { + responseError(w, err) + return + } + + counter, err := session.KeyCounter() + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + // Javascript uses int32, thus getting a string counter for Javascript BigInt to parse. + "Counter": strconv.FormatInt(counter, 10), + }) +}// }}} func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{ // Append index.html if needed for further reading of the file diff --git a/node.go b/node.go index 29aad65..045b441 100644 --- a/node.go +++ b/node.go @@ -12,6 +12,7 @@ type Node struct { ID int UserID int `db:"user_id"` ParentID int `db:"parent_id"` + CryptoKeyID int `db:"crypto_key_id"` Name string Content string Updated time.Time @@ -138,6 +139,7 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{ id, user_id, COALESCE(parent_id, 0) AS parent_id, + COALESCE(crypto_key_id, 0) AS crypto_key_id, name, content, 0 AS level @@ -152,6 +154,7 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{ n.id, n.user_id, n.parent_id, + COALESCE(n.crypto_key_id, 0) AS crypto_key_id, n.name, '' AS content, r.level + 1 AS level @@ -185,20 +188,22 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{ } if row.Level == 0 { - node.ID = row.ID - node.UserID = row.UserID - node.ParentID = row.ParentID - node.Name = row.Name - node.Content = row.Content - node.Complete = true + node.ID = row.ID + node.UserID = row.UserID + node.ParentID = row.ParentID + node.CryptoKeyID = row.CryptoKeyID + node.Name = row.Name + node.Content = row.Content + node.Complete = true } if row.Level == 1 { node.Children = append(node.Children, Node{ - ID: row.ID, - UserID: row.UserID, - ParentID: row.ParentID, - Name: row.Name, + ID: row.ID, + UserID: row.UserID, + ParentID: row.ParentID, + CryptoKeyID: row.CryptoKeyID, + Name: row.Name, }) } } @@ -281,13 +286,23 @@ func (session Session) CreateNode(parentID int, name string) (node Node, err err node.Crumbs, err = session.NodeCrumbs(node.ID) return }// }}} -func (session Session) UpdateNode(nodeID int, content string) (err error) {// {{{ +func (session Session) UpdateNode(nodeID int, content string, cryptoKeyID int) (err error) {// {{{ _, err = db.Exec(` - UPDATE node SET content = $1 WHERE user_id = $2 AND id = $3 + UPDATE node + SET + content = $1, + crypto_key_id = CASE $2::int + WHEN 0 THEN NULL + ELSE $2 + END + WHERE + id = $3 AND + user_id = $4 `, content, - session.UserID, + cryptoKeyID, nodeID, + session.UserID, ) return }// }}} diff --git a/sql/0008.sql b/sql/0008.sql new file mode 100644 index 0000000..d85edc2 --- /dev/null +++ b/sql/0008.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.node ADD crypto_key_id int4 NULL; +ALTER TABLE public.node ADD CONSTRAINT crypto_key_fk FOREIGN KEY (crypto_key_id) REFERENCES public.crypto_key(id) ON DELETE RESTRICT ON UPDATE RESTRICT; diff --git a/sql/0009.sql b/sql/0009.sql new file mode 100644 index 0000000..7b172b5 --- /dev/null +++ b/sql/0009.sql @@ -0,0 +1 @@ +CREATE SEQUENCE aes_ccm_counter AS int8 INCREMENT BY 1 NO CYCLE; diff --git a/sql/0010.sql b/sql/0010.sql new file mode 100644 index 0000000..c461d9d --- /dev/null +++ b/sql/0010.sql @@ -0,0 +1 @@ +ALTER TABLE public.crypto_key ADD CONSTRAINT crypto_user_description_un UNIQUE (user_id, description); diff --git a/static/css/main.css b/static/css/main.css index 1650745..2ce59f2 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -20,14 +20,15 @@ body { } h1 { margin-top: 0px; - font-size: 1em; + font-size: 1.5em; } h2 { margin-top: 32px; - font-size: 0.9em; + font-size: 1.25em; } button { font-size: 1em; + padding: 6px; } #blackout { position: absolute; @@ -76,7 +77,6 @@ button { } #upload input { border: 1px solid #000; - font-size: 0.85em; } #upload .files { display: grid; @@ -274,6 +274,9 @@ header .menu { margin-bottom: 32px; font-size: 1.5em; } +#node-content.encrypted { + color: #a00; +} .node-content { grid-area: content; justify-self: center; @@ -349,7 +352,6 @@ header .menu { grid-template-columns: 1fr min-content; grid-gap: 8px 16px; color: #444; - font-size: 0.85em; } #file-section .files .filename { white-space: nowrap; @@ -373,11 +375,10 @@ header .menu { } #keys .key-list { display: grid; - grid-template-columns: min-content 1fr min-content; + grid-template-columns: min-content min-content min-content; grid-gap: 12px 12px; align-items: end; margin-top: 16px; - font-size: 0.85em; } #keys .key-list .status { cursor: pointer; @@ -396,12 +397,15 @@ header .menu { padding-bottom: 4px; user-select: none; text-decoration: underline; + white-space: nowrap; } #keys .key-list .view { - white-space: nowrap; cursor: pointer; + padding-bottom: 4px; + margin-left: 16px; user-select: none; text-decoration: underline; + white-space: nowrap; } #keys .key-list .hex-key { grid-column: 1 / -1; @@ -409,6 +413,30 @@ header .menu { padding: 16px; margin-bottom: 16px; } +#key-create .fields { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 16px; + max-width: 48ex; +} +#key-create .fields #key-description { + grid-column: 1 / -1; + font-size: 1em; + font-family: monospace; +} +#key-create .fields #key-key { + grid-column: 1 / -1; + height: 4em; + resize: none; + font-size: 1em; +} +#key-create .fields #key-pass1, +#key-create .fields #key-pass2 { + font-size: 1em; +} +#key-create .fields button.generate { + margin-right: 16px; +} .layout-tree { display: grid; grid-template-areas: "header header" "tree crumbs" "tree child-nodes" "tree name" "tree content" "tree files" "tree blank"; diff --git a/static/images/padlock-black.svg b/static/images/padlock-black.svg new file mode 100644 index 0000000..c556b6a --- /dev/null +++ b/static/images/padlock-black.svg @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/static/js/crypto.mjs b/static/js/crypto.mjs index c81a3f8..9aab255 100644 --- a/static/js/crypto.mjs +++ b/static/js/crypto.mjs @@ -1,18 +1,20 @@ export default class Crypto { - constructor(key) { + constructor(key) {//{{{ + if(key === null) + throw new Error("No key provided") + 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() { + static generate_key() {//{{{ return sjcl.random.randomWords(8) - } - - static pass_to_key(pass, salt = null) { + }//}}} + 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) @@ -21,9 +23,9 @@ export default class Crypto { salt, key, } - } + }//}}} - encrypt(plaintext_data_in_bits, counter, return_encoded = true) { + 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). @@ -51,9 +53,8 @@ export default class Crypto { ) else return iv.concat(encrypted) - } - - decrypt(encrypted_base64_data) { + }//}}} + 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 @@ -65,5 +66,7 @@ export default class Crypto { else throw(err) } - } + }//}}} } + +// vim: foldmethod=marker diff --git a/static/js/key.mjs b/static/js/key.mjs index 73bd6a3..4cc8f28 100644 --- a/static/js/key.mjs +++ b/static/js/key.mjs @@ -7,9 +7,13 @@ const html = htm.bind(h) export class Keys extends Component { constructor(props) {//{{{ super(props) - this.retrieveKeys() + this.state = { + create: false, + } + + props.nodeui.retrieveKeys() }//}}} - render({ nodeui }) {//{{{ + render({ nodeui }, { create }) {//{{{ let keys = nodeui.keys.value .sort((a,b)=>{ if(a.description < b.description) return -1 @@ -20,30 +24,127 @@ export class Keys extends Component { html`<${KeyComponent} key=${`key-${key.ID}`} model=${key} />` ) + let createButton = '' + let createComponents = '' + if(create) { + createComponents = html` +
+

New key

+ +
+ + + + + + + +
+ + +
+
+
+ ` + } else { + createButton = html`
` + } + return html`
-

Keys

+

Encryption keys

+

+ Unlock a key by clicking its name. Lock it by clicking it again. +

+ +

+ 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. +

+ +

Click "View key" after unlocking it.

+ + ${createButton} + ${createComponents} + +

Keys

${keys}
` }//}}} - 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) + 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) {//{{{ - this.ID = data.ID - this.description = data.Description - this.unlockedKey = data.Key - this.key = null + 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) @@ -59,17 +160,26 @@ export class Key { window.sessionStorage.removeItem(`key-${this.ID}`) }//}}} unlock(password) {//{{{ - let db = sjcl.codec.hex.toBits(this.unlockedKey) + 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 { - render({ model }) {//{{{ + constructor({ model }) {//{{{ + super({ model }) + this.state = { + show_key: false, + } + }//}}} + render({ model }, { show_key }) {//{{{ let status = '' switch(model.status()) { case 'locked': @@ -81,9 +191,24 @@ export class KeyComponent extends Component { break } + let hex_key = '' + if(show_key) { + if(model.status() == 'locked') + hex_key = html`
Unlock key first
` + else { + let key = sjcl.codec.hex.fromBits(model.key) + key = key.replace(/(....)/g, "$1 ").trim() + hex_key = html`
${key}
` + } + } + + let unlocked = model.status()=='unlocked' + return html`
this.toggle()}>${status}
this.toggle()}>${model.description}
+
this.toggleViewKey()}>${unlocked ? 'View key' : ''}
+ ${hex_key} ` }//}}} toggle() {//{{{ @@ -108,6 +233,9 @@ export class KeyComponent extends Component { alert(err) } }//}}} + toggleViewKey() {//{{{ + this.setState({ show_key: !this.state.show_key }) + }//}}} } // vim: foldmethod=marker diff --git a/static/js/node.mjs b/static/js/node.mjs index 16a93de..bec4d80 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -2,11 +2,12 @@ import { h, Component, createRef } from 'preact' import htm from 'htm' import { signal } from 'preact/signals' import { Keys, Key } from 'key' +import Crypto from 'crypto' const html = htm.bind(h) export class NodeUI extends Component { - constructor() {//{{{ - super() + constructor(props) {//{{{ + super(props) this.menu = signal(false) this.node = signal(null) this.nodeContent = createRef() @@ -53,13 +54,19 @@ export class NodeUI extends Component { let page = '' switch(this.page.value) { case 'node': - if(node.ID > 0) + if(node.ID > 0) { + let padlock = '' + if(node.CryptoKeyID > 0) + padlock = html`` page = html` ${children.length > 0 ? html`
${children}
` : html``} -
${node.Name}
- <${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} /> +
+ ${node.Name} ${padlock} +
+ <${NodeContent} key=${node.ID} node=${node} ref=${this.nodeContent} /> <${NodeFiles} node=${this.node.value} /> ` + } break case 'upload': @@ -84,8 +91,8 @@ export class NodeUI extends Component {
this.saveNode()}>
document.getElementById('app').classList.toggle('toggle-tree')} />
Notes
-
this.createNode(evt)}>+
-
this.showPage('keys')}>
+
this.createNode(evt)}>
+
{ evt.stopPropagation(); this.showPage('keys')}}>
@@ -96,7 +103,11 @@ export class NodeUI extends Component { ${page} ` }//}}} - componentDidMount() {//{{{ + async componentDidMount() {//{{{ + // When rendered and fetching the node, keys could be needed in order to + // decrypt the content. + await this.retrieveKeys() + this.props.app.startNode.retrieve(node=>{ this.node.value = node @@ -177,6 +188,7 @@ export class NodeUI extends Component { node.retrieve(node=>{ this.props.app.nodeModified.value = false this.node.value = node + this.showPage('node') // Tree needs to know another node is selected, in order to render any // previously selected node not selected. @@ -218,6 +230,29 @@ export class NodeUI extends Component { }) }//}}} + async retrieveKeys() {//{{{ + return new Promise((resolve, reject)=>{ + this.props.app.request('/key/retrieve', {}) + .then(res=>{ + this.keys.value = res.Keys.map(keyData=>new Key(keyData, this.keyCounter)) + resolve(this.keys.value) + }) + .catch(reject) + }) + }//}}} + keyCounter() {//{{{ + return window._app.current.request('/key/counter', {}) + .then(res=>BigInt(res.Counter)) + .catch(window._app.current.responseError) + }//}}} + getKey(id) {//{{{ + let keys = this.keys.value + for(let i = 0; i < keys.length; i++) + if(keys[i].ID == id) + return keys[i] + return null + }//}}} + showPage(pg) {//{{{ this.page.value = pg }//}}} @@ -229,10 +264,18 @@ class NodeContent extends Component { this.contentDiv = createRef() this.state = { modified: false, - //content: props.content, } }//}}} - render({ content }) {//{{{ + render({ node }) {//{{{ + let content = '' + try { + content = node.content() + } catch(err) { + return html` +
${err.message}
+ ` + } + return html`
@@ -255,6 +298,18 @@ class NodeContent extends Component { if(textarea) textarea.parentNode.dataset.replicatedValue = textarea.value }//}}} + unlock() {//{{{ + let pass = prompt("Password") + if(!pass) + return + + try { + this.props.model.unlock(pass) + this.forceUpdate() + } catch(err) { + alert(err) + } + }//}}} } class NodeFiles extends Component { @@ -299,11 +354,13 @@ export class Node { this.ID = nodeID this.ParentID = 0 this.UserID = 0 + this.CryptoKeyID = 0 this.Name = '' - this.Content = '' + this._content = '' this.Children = [] this.Crumbs = [] this.Files = [] + this._decrypted = false this._expanded = false // start value for the TreeNode component, // it doesn't control it afterwards. // Used to expand the crumbs upon site loading. @@ -311,13 +368,14 @@ export class Node { retrieve(callback) {//{{{ this.app.request('/node/retrieve', { ID: this.ID }) .then(res=>{ - this.ParentID = res.Node.ParentID - this.UserID = res.Node.UserID - this.Name = res.Node.Name - this.Content = res.Node.Content - this.Children = res.Node.Children - this.Crumbs = res.Node.Crumbs - this.Files = res.Node.Files + this.ParentID = res.Node.ParentID + this.UserID = res.Node.UserID + this.CryptoKeyID = res.Node.CryptoKeyID + this.Name = res.Node.Name + this._content = res.Node.Content + this.Children = res.Node.Children + this.Crumbs = res.Node.Crumbs + this.Files = res.Node.Files callback(this) }) .catch(this.app.responseError) @@ -339,10 +397,18 @@ export class Node { }) .catch(this.app.responseError) }//}}} - save(content, callback) {//{{{ + async save(content, callback) {//{{{ + let update_content = content + /* + * XXX - fix encrypting when saving + if(this.CryptoKeyID != 0) + update_content = await this.#encrypt(content) + */ + this.app.request('/node/update', { NodeID: this.ID, - Content: content, + Content: update_content, + CryptoKeyID: this.CryptoKeyID, }) .then(callback) .catch(this.app.responseError) @@ -387,6 +453,74 @@ export class Node { a.remove() //afterwards we remove the element again }) }//}}} + content() {//{{{ + if(this.CryptoKeyID != 0 && !this._decrypted) { + this.#decrypt() + } + return this._content + }//}}} + + async encrypt(obj_key) {//{{{ + if(obj_key.ID != this.CryptoKeyID) + throw('Invalid key') + + let crypto = new Crypto(obj_key.key) + this._decrypted = false + + let counter = await obj_key.counter() + + this.content = sjcl.codec.base64.fromBits( + crypto.encrypt( + sjcl.codec.utf8String.toBits(this.content), + counter, + false, + ) + ) + }//}}} + #decrypt() {//{{{ + let obj_key = this.app.nodeUI.current.getKey(this.CryptoKeyID) + if(obj_key === null || obj_key.ID != this.CryptoKeyID) + throw('Invalid key') + + // Ask user to unlock key first + var pass = null + while(pass || obj_key.status() == 'locked') { + pass = prompt("Password") + if(!pass) + throw new Error(`Key "${obj_key.description}" is locked`) + + try { + obj_key.unlock(pass) + } catch(err) { + alert(err) + } + pass = null + } + + if(obj_key.status() == 'locked') + throw new Error(`Key "${obj_key.description}" is locked`) + + let crypto = new Crypto(obj_key.key) + this._decrypted = true + this._content = sjcl.codec.utf8String.fromBits( + crypto.decrypt(this._content) + ) + }//}}} + /* + async #encrypt(content) {//{{{ + let obj_key = this.app.nodeUI.current.getKey(this.CryptoKeyID) + if(obj_key === null || obj_key.ID != this.CryptoKeyID) + throw('Invalid key') + + if(obj_key.status() == 'locked') + throw new Error(`Key "${obj_key.description}" is locked`) + + let crypto = new Crypto(obj_key.key) + let content_bits = sjcl.codec.utf8String.toBits(content) + let counter = await this.app.nodeUI.current.keyCounter() + return crypto.encrypt(content_bits, counter, true) + }//}}} + */ } class Menu extends Component { @@ -504,16 +638,82 @@ class UploadUI extends Component { class NodeProperties extends Component { constructor(props) {//{{{ super(props) + this.props.nodeui.retrieveKeys() + this.selected_key_id = 0 }//}}} render({ nodeui }) {//{{{ + let save = true + 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=>{ + this.props.nodeui.keys.value.some(uikey=>{ + if(uikey.ID == nodeui.node.value.ID) { + save = (uikey.status() == 'unlocked') + return true + } + }) + + return html` +
+ this.selected_key_id = key.ID} /> + +
` + }) + return html` -
nodeui.properties.value = false}>
-

Node properties

- +

Note properties

+ + These properties are only for this note. + +

Encryption

+
+ this.selected_key_id = 0} /> + +
+ ${keys} + + ${save ? html`` : ''}
` }//}}} + save() {//{{{ + let nodeui = this.props.nodeui + let node = nodeui.node.value + + // Find the actual key object used for encryption + let encrypt_key = nodeui.getKey(this.selected_key_id) + let decrypt_key = nodeui.getKey(node.CryptoKeyID) + + if(decrypt_key && decrypt_key.status() == 'locked') { + alert("Decryption key is locked and can not be used.") + return + } + + if(encrypt_key && encrypt_key.status() == 'locked') { + alert("Key is locked and can not be used.") + return + } + + // Currently not encrypted - encrypt with new key. + let crypto = new Crypto(selected_key.key) + if(node.CryptoKeyID == 0) { + let encrypted = crypto.encrypt( + sjcl.codec.utf8String.toBits(node.Content()), + 1n, + ) + console.log(encrypted) + } + + /* + crypto.encrypt( + ) + */ + }//}}} } // vim: foldmethod=marker diff --git a/static/less/main.less b/static/less/main.less index 2f48158..5dbe7ce 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -24,16 +24,17 @@ html, body { h1 { margin-top: 0px; - font-size: 1em; + font-size: 1.5em; } h2 { margin-top: 32px; - font-size: 0.9em; + font-size: 1.25em; } button { font-size: 1em; + padding: 6px; } #blackout { @@ -90,7 +91,6 @@ button { input { border: 1px solid #000; - font-size: 0.85em; } .files { @@ -330,6 +330,10 @@ header { font-size: 1.5em; } +#node-content.encrypted { + color: #a00; +} + .node-content { grid-area: content; justify-self: center; @@ -413,7 +417,6 @@ header { grid-template-columns: 1fr min-content; grid-gap: 8px 16px; color: #444; - font-size: 0.85em; .filename { white-space: nowrap; @@ -443,11 +446,10 @@ header { .key-list { display: grid; - grid-template-columns: min-content 1fr min-content; + grid-template-columns: min-content min-content min-content; grid-gap: 12px 12px; align-items: end; margin-top: 16px; - font-size: 0.85em; .status { cursor: pointer; @@ -464,13 +466,16 @@ header { padding-bottom: 4px; user-select: none; text-decoration: underline; + white-space: nowrap; } .view { - white-space: nowrap; cursor: pointer; + padding-bottom: 4px; + margin-left: 16px; user-select: none; text-decoration: underline; + white-space: nowrap; } .hex-key { @@ -482,6 +487,39 @@ header { } } +#key-create { + .fields { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 16px; + max-width: 48ex; + + #key-description { + grid-column: 1 / -1; + font-size: 1em; + font-family: monospace; + } + + #key-key { + grid-column: 1 / -1; + height: 4em; + resize: none; + font-size: 1em; + } + + #key-pass1, #key-pass2 { + font-size: 1em; + } + + button.generate { + margin-right: 16px; + } + + button.create { + } + } +} + .layout-tree {// {{{ display: grid; grid-template-areas: