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`
+
+ `
+ } 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 {
@@ -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: