From 54a0ee4f29af0bc4a0e37bdcbbd4b3d52c8dea19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 20 Jul 2023 10:06:28 +0200 Subject: [PATCH 1/4] Hashed passwords. --- session.go | 2 +- sql/0013.sql | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 sql/0013.sql diff --git a/session.go b/session.go index 2457eb3..53d163e 100644 --- a/session.go +++ b/session.go @@ -91,7 +91,7 @@ func (session *Session) Authenticate(username, password string) (authenticated b FROM public.user WHERE username=$1 AND - password=$2 + password=password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea) `, username, password, diff --git a/sql/0013.sql b/sql/0013.sql new file mode 100644 index 0000000..d8fb23d --- /dev/null +++ b/sql/0013.sql @@ -0,0 +1,34 @@ +/* Required for the gen_random_bytes function */ +CREATE EXTENSION pgcrypto; + +CREATE FUNCTION password_hash(salt_hex char(32), pass bytea) +RETURNS char(96) +LANGUAGE plpgsql +AS +$$ +BEGIN + RETURN ( + SELECT + salt_hex || + encode( + sha256( + decode(salt_hex, 'hex') || /* salt in binary */ + pass /* password */ + ), + 'hex' + ) + ); +END; +$$; + +/* Password has to be able to accommodate 96 characters instead of previous 64. + * It can't be char(96), because then the password would be padded to 96 characters. */ +ALTER TABLE public."user" ALTER COLUMN "password" TYPE varchar(96) USING "password"::varchar; + +/* Update all users with salted and hashed passwords */ +UPDATE public.user +SET password = password_hash( encode(gen_random_bytes(16),'hex'), password::bytea); + +/* After the password hashing, all passwords are now hex encoded 32 characters salt and 64 characters hash, + * and the varchar type is not longer necessary. */ +ALTER TABLE public."user" ALTER COLUMN "password" TYPE char(96) USING "password"::varchar; From 48252de9f3554bcb13ac622b2de56c0fd702749e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 20 Jul 2023 10:07:47 +0200 Subject: [PATCH 2/4] Search CSS --- static/css/main.css | 21 ++++---- static/images/search.svg | 107 +++++++++++++++++++++++++++++++++++++++ static/less/main.less | 22 ++++---- 3 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 static/images/search.svg diff --git a/static/css/main.css b/static/css/main.css index 63d21b9..d025926 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -55,6 +55,13 @@ button { border: 2px solid #000; box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.5); z-index: 1025; + width: min-content; +} +#menu .section { + padding: 16px 16px 0px 16px; + font-weight: bold; + white-space: nowrap; + color: #c84a37; } #menu .item { padding: 16px; @@ -137,7 +144,7 @@ button { header { display: grid; grid-area: header; - grid-template-columns: min-content 1fr repeat(3, min-content); + grid-template-columns: min-content 1fr repeat(4, min-content); align-items: center; padding: 8px 0px; color: #333c11; @@ -161,17 +168,13 @@ header .name { padding-left: 16px; font-size: 1.25em; } -header .add { - padding-right: 16px; - cursor: pointer; -} -header .add img { - cursor: pointer; - height: 24px; -} +header .search, +header .add, header .keys { padding-right: 16px; } +header .search img, +header .add img, header .keys img { cursor: pointer; height: 24px; diff --git a/static/images/search.svg b/static/images/search.svg new file mode 100644 index 0000000..28b3b5f --- /dev/null +++ b/static/images/search.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/static/less/main.less b/static/less/main.less index 7435f57..0a54562 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -56,6 +56,14 @@ button { border: 2px solid #000; box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.5); z-index: 1025; + width: min-content; + + .section { + padding: 16px 16px 0px 16px; + font-weight: bold; + white-space: nowrap; + color: @accent_3; + } .item { padding: 16px; @@ -154,7 +162,7 @@ button { header { display: grid; grid-area: header; - grid-template-columns: min-content 1fr repeat(3, min-content); + grid-template-columns: min-content 1fr repeat(4, min-content); align-items: center; padding: 8px 0px; color: darken(@accent_1, 35%); @@ -181,17 +189,7 @@ header { font-size: 1.25em; } - .add { - padding-right: 16px; - cursor: pointer; - - img { - cursor: pointer; - height: 24px; - } - } - - .keys { + .search, .add, .keys { padding-right: 16px; img { From db33be9a37602dcd54750a070757fb5ff68e5f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 20 Jul 2023 10:47:49 +0200 Subject: [PATCH 3/4] Password update --- main.go | 64 ++++++++++++++++++++------- static/css/main.css | 13 ++++++ static/js/node.mjs | 100 +++++++++++++++++++++++++++++++++++++++--- static/less/main.less | 16 +++++++ user.go | 37 ++++++++++++++++ 5 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 user.go diff --git a/main.go b/main.go index d808ea3..5553690 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ import ( const VERSION = "v10"; const LISTEN_HOST = "0.0.0.0"; -const DB_SCHEMA = 12 +const DB_SCHEMA = 13 var ( flagPort int @@ -64,22 +64,23 @@ func main() {// {{{ go connectionManager.BroadcastLoop() static = http.FileServer(http.Dir(config.Application.Directories.Static)) - http.HandleFunc("/css_updated", cssUpdateHandler) - http.HandleFunc("/session/create", sessionCreate) - http.HandleFunc("/session/retrieve", sessionRetrieve) + http.HandleFunc("/css_updated", cssUpdateHandler) + http.HandleFunc("/session/create", sessionCreate) + http.HandleFunc("/session/retrieve", sessionRetrieve) http.HandleFunc("/session/authenticate", sessionAuthenticate) - http.HandleFunc("/node/tree", nodeTree) - http.HandleFunc("/node/retrieve", nodeRetrieve) - http.HandleFunc("/node/create", nodeCreate) - http.HandleFunc("/node/update", nodeUpdate) - http.HandleFunc("/node/rename", nodeRename) - http.HandleFunc("/node/delete", nodeDelete) - http.HandleFunc("/node/upload", nodeUpload) - http.HandleFunc("/node/download", nodeDownload) - http.HandleFunc("/node/search", nodeSearch) - http.HandleFunc("/key/retrieve", keyRetrieve) - http.HandleFunc("/key/create", keyCreate) - http.HandleFunc("/key/counter", keyCounter) + http.HandleFunc("/user/password", userPassword) + http.HandleFunc("/node/tree", nodeTree) + http.HandleFunc("/node/retrieve", nodeRetrieve) + http.HandleFunc("/node/create", nodeCreate) + http.HandleFunc("/node/update", nodeUpdate) + http.HandleFunc("/node/rename", nodeRename) + http.HandleFunc("/node/delete", nodeDelete) + http.HandleFunc("/node/upload", nodeUpload) + http.HandleFunc("/node/download", nodeDownload) + http.HandleFunc("/node/search", nodeSearch) + http.HandleFunc("/key/retrieve", keyRetrieve) + http.HandleFunc("/key/create", keyCreate) + http.HandleFunc("/key/counter", keyCounter) http.HandleFunc("/ws", websocketHandler) http.HandleFunc("/", staticHandler) @@ -201,6 +202,37 @@ func sessionAuthenticate(w http.ResponseWriter, r *http.Request) {// {{{ }) }// }}} +func userPassword(w http.ResponseWriter, r *http.Request) {// {{{ + var err error + var ok bool + var session Session + + if session, _, err = ValidateSession(r, true); err != nil { + responseError(w, err) + return + } + + req := struct { + CurrentPassword string + NewPassword string + }{} + if err = parseRequest(r, &req); err != nil { + responseError(w, err) + return + } + + ok, err = session.UpdatePassword(req.CurrentPassword, req.NewPassword) + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + "CurrentPasswordOK": ok, + }) +}// }}} + func nodeTree(w http.ResponseWriter, r *http.Request) {// {{{ var err error var session Session diff --git a/static/css/main.css b/static/css/main.css index d025926..a92b7c7 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -541,6 +541,19 @@ header .menu { #app.node.toggle-tree #tree { display: none; } +#profile-settings { + color: #333; + padding: 16px; +} +#profile-settings .passwords { + display: grid; + grid-template-columns: min-content 200px; + grid-gap: 8px 16px; + margin-bottom: 16px; +} +#profile-settings .passwords div { + white-space: nowrap; +} @media only screen and (max-width: 932px) { #app.node { grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "files" "blank"; diff --git a/static/js/node.mjs b/static/js/node.mjs index 282614c..a6f2d25 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -86,6 +86,10 @@ export class NodeUI extends Component { page = html`<${Keys} nodeui=${this} />` break + case 'profile-settings': + page = html`<${ProfileSettings} nodeui=${this} />` + break + case 'search': page = html`<${Search} nodeui=${this} />` break @@ -100,9 +104,10 @@ export class NodeUI extends Component {
this.saveNode()}>
document.getElementById('app').classList.toggle('toggle-tree')} />
Notes
-
this.createNode(evt)}>
-
{ evt.stopPropagation(); this.showPage('keys')}}>
- + +
this.createNode(evt)}>
+
{ evt.stopPropagation(); this.showPage('keys')}}>
+
@@ -213,7 +218,11 @@ export class NodeUI extends Component { this.node.value.create(name, nodeID=>this.goToNode(nodeID)) }//}}} saveNode() {//{{{ - let content = this.nodeContent.current.contentDiv.current.value + let nodeContent = this.nodeContent.current + if(this.page.value != 'node' || nodeContent === null) + return + + let content = nodeContent.contentDiv.current.value this.node.value.setContent(content) this.node.value.save(()=>this.props.app.nodeModified.value = false) }//}}} @@ -547,10 +556,14 @@ class Menu extends Component { return html`
nodeui.menu.value = false}>
` @@ -788,4 +801,77 @@ class Search extends Component { }//}}} } +class ProfileSettings extends Component { + render({ nodeui }, {}) {//{{{ + return html` +
+

User settings

+ +

Password

+
+
Current
+ this.keyHandler(evt)} /> + +
New
+ this.keyHandler(evt)} /> + +
Repeat
+ this.keyHandler(evt)} /> +
+ + +
` + }//}}} + componentDidMount() {//{{{ + document.getElementById('current-password').focus() + }//}}} + + keyHandler(evt) {//{{{ + let handled = true + + switch(evt.key.toUpperCase()) { + case 'ENTER': + this.updatePassword() + break + + default: + handled = false + } + + if(handled) { + evt.preventDefault() + evt.stopPropagation() + } + }//}}} + updatePassword() {//{{{ + let curr_pass = document.getElementById('current-password').value + let pass1 = document.getElementById('new-password1').value + let pass2 = document.getElementById('new-password2').value + + try { + if(pass1.length < 4) { + throw new Error('Password has to be at least 4 characters long') + } + + if(pass1 != pass2) { + throw new Error(`Passwords don't match`) + } + + window._app.current.request('/user/password', { + CurrentPassword: curr_pass, + NewPassword: pass1, + }) + .then(res=>{ + if(res.CurrentPasswordOK) + alert('Password is changed successfully') + else + alert('Current password is invalid') + }) + } catch(err) { + alert(err.message) + } + + }//}}} +} + // vim: foldmethod=marker diff --git a/static/less/main.less b/static/less/main.less index 0a54562..f9fa33c 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -615,6 +615,22 @@ header { } +#profile-settings { + color: #333; + padding: 16px; + + .passwords { + display: grid; + grid-template-columns: min-content 200px; + grid-gap: 8px 16px; + margin-bottom: 16px; + + div { + white-space: nowrap; + } + } +} + @media only screen and (max-width: 932px) { #app.node { .layout-crumbs(); diff --git a/user.go b/user.go new file mode 100644 index 0000000..0d9b4ee --- /dev/null +++ b/user.go @@ -0,0 +1,37 @@ +package main + +import ( + // Standard + "database/sql" +) + +func (session Session) UpdatePassword(currPass, newPass string) (ok bool, err error) { + var result sql.Result + var rowsAffected int64 + + result, err = db.Exec(` + UPDATE public.user + SET + password = password_hash( + /* salt in hex */ + ENCODE(gen_random_bytes(16), 'hex'), + + /* password */ + $1::bytea + ) + WHERE + id = $2 AND + password=password_hash(SUBSTRING(password FROM 1 FOR 32), $3::bytea) + RETURNING id + `, + newPass, + session.UserID, + currPass, + ) + + if rowsAffected, err = result.RowsAffected(); err != nil { + return + } + + return rowsAffected > 0, nil +} From 63a73705707d2ff77cb222723bf5029af6d7e275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 20 Jul 2023 10:52:15 +0200 Subject: [PATCH 4/4] Bumped to v11, display version in web-UI --- main.go | 2 +- static/css/main.css | 5 +++++ static/js/node.mjs | 2 +- static/less/main.less | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 5553690..67b2fa7 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ import ( _ "embed" ) -const VERSION = "v10"; +const VERSION = "v11"; const LISTEN_HOST = "0.0.0.0"; const DB_SCHEMA = 13 diff --git a/static/css/main.css b/static/css/main.css index a92b7c7..552928f 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -279,6 +279,11 @@ header .menu { margin-bottom: 32px; font-size: 1.5em; } +#notes-version { + margin-top: 64px; + color: #888; + text-align: center; +} #node-content.encrypted { color: #a00; } diff --git a/static/js/node.mjs b/static/js/node.mjs index a6f2d25..6f1b4bf 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -57,7 +57,7 @@ export class NodeUI extends Component { case 'node': if(node.ID == 0) { page = html` - ${children.length > 0 ? html`
${children}
` : html``} + ${children.length > 0 ? html`
${children}
Notes version ${window._VERSION}
` : html``} ` } else { let padlock = '' diff --git a/static/less/main.less b/static/less/main.less index f9fa33c..c885ec8 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -322,6 +322,12 @@ header { font-size: 1.5em; } +#notes-version { + margin-top: 64px; + color: #888; + text-align: center; +} + #node-content.encrypted { color: #a00; }