support SRP authentication

Massimo Melina committed Jan 7, 2022 at 14:49 UTC ed57b313311478c29ac5b9494abba49cab9a2b14
12 files changed +215 -58
frontend/package-lock.json
+22
@@ -8,10 +8,12 @@
8 "name": "frontend",
9 "version": "0.4.0",
10 "dependencies": {
11 + "js-sha512": "^0.8.0",
12 "lodash": "^4.17.21",
13 "react": "^17.0.2",
14 "react-dom": "^17.0.2",
15 "react-router-dom": "^6.1.1",
16 + "tssrp6a": "^3.0.0",
17 "use-debounce": "^7.0.1",
18 "valtio": "^1.2.7",
19 "web-vitals": "^1.0.1"
@@ -11150,6 +11152,11 @@
11152 "optional": true,
11153 "peer": true
11154 },
11155 + "node_modules/js-sha512": {
11156 + "version": "0.8.0",
11157 + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz",
11158 + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="
11159 + },
11160 "node_modules/js-tokens": {
11161 "version": "4.0.0",
11162 "license": "MIT"
@@ -17096,6 +17103,11 @@
17103 "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
17104 "dev": true
17105 },
17106 + "node_modules/tssrp6a": {
17107 + "version": "3.0.0",
17108 + "resolved": "https://registry.npmjs.org/tssrp6a/-/tssrp6a-3.0.0.tgz",
17109 + "integrity": "sha512-kGvFhbBfQhiAhGrVO++QFpyVLqk2dy32oEcUvc8ymkuRTufSLZwQqp/QpYbBItQ9Wr1p1IC61AS9dBpGOTr7dQ=="
17110 + },
17111 "node_modules/tsutils": {
17112 "version": "3.21.0",
17113 "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
@@ -26769,6 +26781,11 @@
26781 "optional": true,
26782 "peer": true
26783 },
26784 + "js-sha512": {
26785 + "version": "0.8.0",
26786 + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz",
26787 + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="
26788 + },
26789 "js-tokens": {
26790 "version": "4.0.0"
26791 },
@@ -31032,6 +31049,11 @@
31049 "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
31050 "dev": true
31051 },
31052 + "tssrp6a": {
31053 + "version": "3.0.0",
31054 + "resolved": "https://registry.npmjs.org/tssrp6a/-/tssrp6a-3.0.0.tgz",
31055 + "integrity": "sha512-kGvFhbBfQhiAhGrVO++QFpyVLqk2dy32oEcUvc8ymkuRTufSLZwQqp/QpYbBItQ9Wr1p1IC61AS9dBpGOTr7dQ=="
31056 + },
31057 "tsutils": {
31058 "version": "3.21.0",
31059 "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
frontend/package.json
+4 -2
@@ -11,13 +11,15 @@
11 "eject": "react-scripts eject"
12 },
13 "dependencies": {
14 + "js-sha512": "^0.8.0",
15 + "lodash": "^4.17.21",
16 "react": "^17.0.2",
17 "react-dom": "^17.0.2",
18 "react-router-dom": "^6.1.1",
19 + "tssrp6a": "^3.0.0",
20 "use-debounce": "^7.0.1",
21 "valtio": "^1.2.7",
19 - "web-vitals": "^1.0.1",
20 - "lodash": "^4.17.21"
22 + "web-vitals": "^1.0.1"
23 },
24 "devDependencies": {
25 "@testing-library/jest-dom": "^5.11.4",
frontend/src/index.tsx
+1
@@ -1,6 +1,7 @@
1 import React from 'react';
2 import ReactDOM from 'react-dom';
3 import './index.scss';
4 +import './min-crypto-polyfill'
5 import App from './App';
6 import reportWebVitals from './reportWebVitals';
7
frontend/src/login.ts
+22 -1
@@ -1,6 +1,7 @@
1 import { apiCall, ApiError } from './api'
2 import { state } from './state'
3 import { alertDialog } from './dialog'
4 +import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
5
6 let refresher: NodeJS.Timeout
7
@@ -8,8 +9,28 @@ export async function login(user:string, password:string) {
9 if (refresher)
10 clearInterval(refresher)
11 try {
12 +/* simple login without encryption. Here commented just for example. Please use SRP version.
13 const res = await apiCall('login', { user, password })
12 - sessionRefresher(res)
14 +*/
15 + const { pubKey, salt } = await apiCall('loginSrp1', { user })
16 + if (!salt) return
17 +
18 + const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
19 + const srp = new SRPClientSession(srp6aNimbusRoutines);
20 + const resStep1 = await srp.step1(user, password)
21 + const resStep2 = await resStep1.step2(BigInt(salt), BigInt(pubKey))
22 + const res = await apiCall('loginSrp2', { pubKey: String(resStep2.A), proof: String(resStep2.M1) }) // bigint-s must be cast to string to be json-ed
23 + try {
24 + await resStep2.step3(BigInt(res.proof))
25 + }
26 + catch(e){
27 + console.debug(String(e))
28 + await alertDialog("Server identity cannot be trusted. Login aborted.", 'error')
29 + return
30 + }
31 +
32 + // login was successful, update state
33 + sessionRefresher({ user, exp:res.exp })
34 state.username = user
35 }
36 catch(err) {
frontend/src/menu.ts
+8 -1
@@ -6,6 +6,7 @@ import { hIcon, prefix } from './misc'
6 import { login, logout } from './login'
7 import { apiCall } from './api'
8 import { Checkbox, FlexV } from './components'
9 +import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
10
11 export function MenuPanel() {
12 const { remoteSearch, stopSearch, stoppedSearch, listFilter } = useSnapState()
@@ -153,7 +154,13 @@ function UserPanel() {
154 if (!check) return
155 if (check !== pwd)
156 return alertDialog('The second password you entered did not match the first. Procedure aborted.', 'warning')
156 - await apiCall('change_pwd', { newPassword: pwd })
157 + const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
158 + const res = await createVerifierAndSalt(srp6aNimbusRoutines, snap.username, pwd)
159 + await apiCall('change_srp', { salt: String(res.s), verifier: String(res.v) }).catch(e => {
160 + if (e.code !== 406) // 406 = server was configured to support clear text authentication
161 + throw e
162 + return apiCall('change_password', { newPassword: pwd }) // unencrypted version
163 + })
164 return alertDialog('Password changed')
165 }
166 }),
frontend/src/min-crypto-polyfill.js new
+19
@@ -0,0 +1,19 @@
1 +export {}
2 +// this is the minimum required for lib tssrp6a to work
3 +if (!window.crypto?.subtle) {
4 + console.debug('poly subtle')
5 +
6 + const subtle = {
7 + async digest(algo, buff) {
8 + if (algo !== 'SHA-512')
9 + return alert(algo + ' required but not supported')
10 + const lib = await import('js-sha512')
11 + const sha = lib.default.arrayBuffer
12 + return sha(buff)
13 + }
14 + }
15 + if (!window.crypto)
16 + window.crypto = { subtle }
17 + if (!crypto.subtle)
18 + crypto.subtle = subtle
19 +}
package-lock.json
+31 -20
@@ -21,7 +21,8 @@
21 "koa-session": "^6.2.0",
22 "lodash": "^4.17.21",
23 "minimist": "^1.2.5",
24 - "yaml": "^1.10.2"
24 + "tssrp6a": "^3.0.0",
25 + "yaml": "^2.0.0-10"
26 },
27 "bin": {
28 "hfs": "dist/index.js"
@@ -1867,9 +1868,9 @@
1868 }
1869 },
1870 "node_modules/hosted-git-info": {
1870 - "version": "4.0.2",
1871 - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz",
1872 - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==",
1871 + "version": "4.1.0",
1872 + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
1873 + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
1874 "dev": true,
1875 "dependencies": {
1876 "lru-cache": "^6.0.0"
@@ -2050,9 +2051,9 @@
2051 "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ=="
2052 },
2053 "node_modules/is-core-module": {
2053 - "version": "2.8.0",
2054 - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
2055 - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
2054 + "version": "2.8.1",
2055 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
2056 + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
2057 "dev": true,
2058 "dependencies": {
2059 "has": "^1.0.3"
@@ -3852,6 +3853,11 @@
3853 "node": ">=0.6.x"
3854 }
3855 },
3856 + "node_modules/tssrp6a": {
3857 + "version": "3.0.0",
3858 + "resolved": "https://registry.npmjs.org/tssrp6a/-/tssrp6a-3.0.0.tgz",
3859 + "integrity": "sha512-kGvFhbBfQhiAhGrVO++QFpyVLqk2dy32oEcUvc8ymkuRTufSLZwQqp/QpYbBItQ9Wr1p1IC61AS9dBpGOTr7dQ=="
3860 + },
3861 "node_modules/type-fest": {
3862 "version": "0.20.2",
3863 "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -4138,11 +4144,11 @@
4144 "dev": true
4145 },
4146 "node_modules/yaml": {
4141 - "version": "1.10.2",
4142 - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
4143 - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
4147 + "version": "2.0.0-10",
4148 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-10.tgz",
4149 + "integrity": "sha512-FHV8s5ODFFQXX/enJEU2EkanNl1UDBUz8oa4k5Qo/sR+Iq7VmhCDkRMb0/mjJCNeAWQ31W8WV6PYStDE4d9EIw==",
4150 "engines": {
4145 - "node": ">= 6"
4151 + "node": ">= 12"
4152 }
4153 },
4154 "node_modules/yargs": {
@@ -5576,9 +5582,9 @@
5582 "dev": true
5583 },
5584 "hosted-git-info": {
5579 - "version": "4.0.2",
5580 - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz",
5581 - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==",
5585 + "version": "4.1.0",
5586 + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
5587 + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
5588 "dev": true,
5589 "requires": {
5590 "lru-cache": "^6.0.0"
@@ -5711,9 +5717,9 @@
5717 "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ=="
5718 },
5719 "is-core-module": {
5714 - "version": "2.8.0",
5715 - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
5716 - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
5720 + "version": "2.8.1",
5721 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
5722 + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
5723 "dev": true,
5724 "requires": {
5725 "has": "^1.0.3"
@@ -7038,6 +7044,11 @@
7044 "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
7045 "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
7046 },
7047 + "tssrp6a": {
7048 + "version": "3.0.0",
7049 + "resolved": "https://registry.npmjs.org/tssrp6a/-/tssrp6a-3.0.0.tgz",
7050 + "integrity": "sha512-kGvFhbBfQhiAhGrVO++QFpyVLqk2dy32oEcUvc8ymkuRTufSLZwQqp/QpYbBItQ9Wr1p1IC61AS9dBpGOTr7dQ=="
7051 + },
7052 "type-fest": {
7053 "version": "0.20.2",
7054 "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -7255,9 +7266,9 @@
7266 "dev": true
7267 },
7268 "yaml": {
7258 - "version": "1.10.2",
7259 - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
7260 - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
7269 + "version": "2.0.0-10",
7270 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-10.tgz",
7271 + "integrity": "sha512-FHV8s5ODFFQXX/enJEU2EkanNl1UDBUz8oa4k5Qo/sR+Iq7VmhCDkRMb0/mjJCNeAWQ31W8WV6PYStDE4d9EIw=="
7272 },
7273 "yargs": {
7274 "version": "16.2.0",
package.json
+3 -2
@@ -14,7 +14,7 @@
14 "start-frontend": "cd frontend && npm run start",
15 "start": "node dist",
16 "build": "npm install && node timestamp && tsc --target es2018 && cp -r plugins READ* package*.json *.yaml dist && cd dist && npm ci --production && cd .. && npm run build-prune",
17 - "build-prune": "cd dist && nm-prune --force && cd node_modules && rm -rf yaml/browser && mkdir lodash2 && cp lodash/lodash.min.js lodash2/lodash.js && cp lodash/package.json lodash2 && rm -rf lodash && mv lodash2 lodash",
17 + "build-prune": "cd dist/node_modules/yaml/dist && mv doc do_c && cd ../.. && mkdir lodash2 && cp lodash/lodash.min.js lodash2/lodash.js && cp lodash/package.json lodash2 && rm -rf lodash && mv lodash2 lodash && cd .. && nm-prune --force && cd node_modules/yaml/dist && mv do_c doc",
18 "build-all": "rm -rf dist && npm run build && cd frontend && npm install && npm run build && echo COMPLETED",
19 "test": "mocha -r ts-node/register 'tests/**/*.ts'",
20 "make-exe-after-build": "pkg . -C brotli"
@@ -41,7 +41,8 @@
41 "koa-session": "^6.2.0",
42 "lodash": "^4.17.21",
43 "minimist": "^1.2.5",
44 - "yaml": "^1.10.2"
44 + "tssrp6a": "^3.0.0",
45 + "yaml": "^2.0.0-10"
46 },
47 "devDependencies": {
48 "@types/archiver": "^5.1.1",
src/config.ts
+2
@@ -2,6 +2,8 @@ import EventEmitter from 'events'
2 import { argv } from './const'
3 import { watchLoad } from './watchLoad'
4
5 +export const CFG_ALLOW_CLEAR_TEXT_LOGIN = 'allow clear text login'
6 +
7 const PATH = 'config.yaml'
8
9 let state:Record<string,any> = {}
src/frontEndApis.ts
+65 -3
@@ -2,11 +2,19 @@ import { vfs, VfsNode, walkNode } from './vfs'
2 import _ from 'lodash'
3 import createSSE from './sse'
4 import { basename } from 'path'
5 -import { getCurrentUsername, updateAccount, verifyLogin } from './perm'
5 +import { getAccount, getCurrentUsername, saveSrpInfo, updateAccount } from './perm'
6 import { stat } from 'fs/promises'
7 import { ApiHandlers } from './apis'
8 import { plugins } from './plugins'
9 import { PLUGINS_PUB_URI } from './const'
10 +import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
11 +import { SESSION_DURATION } from './index'
12 +import { verifyPassword } from './crypt'
13 +import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig } from './config'
14 +
15 +const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
16 +const srpSession = new SRPServerSession(srp6aNimbusRoutines)
17 +const ongoingLogins:Record<string,SRPServerSessionStep1> = {}
18
19 export const frontEndApis: ApiHandlers = {
20
@@ -65,13 +73,53 @@ export const frontEndApis: ApiHandlers = {
73 return ctx.status = 400
74 if (!password)
75 return ctx.status = 400
68 - if (!await verifyLogin(user, password))
76 + const acc = getAccount(user)
77 + if (!acc)
78 + return ctx.status = 401
79 + if (!acc.hashedPassword)
80 + return ctx.status = 406
81 + if (!await verifyPassword(acc.hashedPassword, password))
82 return ctx.status = 401
83 if (ctx.session)
84 ctx.session.user = user
85 return makeExp()
86 },
87
88 + async loginSrp1({ user }, ctx) {
89 + const account = getAccount(user)
90 + if (!ctx.session)
91 + return ctx.throw(500)
92 + if (!account) // TODO simulate fake account to prevent knowing valid usernames
93 + return ctx.status = 401
94 + if (!account.srp)
95 + return ctx.status = 406 // unacceptable
96 +
97 + const [salt, verifier] = account.srp.split('|')
98 + const step1 = await srpSession.step1(account.user, BigInt(salt), BigInt(verifier))
99 + const sid = Math.random()
100 + ongoingLogins[sid] = step1
101 + setTimeout(()=> delete ongoingLogins[sid], 60_000)
102 +
103 + ctx.session.login = { user, sid }
104 + return { salt, pubKey: String(step1.B) } // cast to string cause bigint can't be jsonized
105 + },
106 +
107 + async loginSrp2({ pubKey, proof }, ctx) {
108 + if (!ctx.session)
109 + return ctx.throw(500)
110 + const { user, sid } = ctx.session.login
111 + const step1 = ongoingLogins[sid]
112 + try {
113 + const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
114 + ctx.session.user = user
115 + return { proof: String(M2), ...makeExp() }
116 + }
117 + catch(e) {
118 + ctx.body = String(e)
119 + ctx.status = 401
120 + }
121 + },
122 +
123 async logout({}, ctx) {
124 if (ctx.session)
125 ctx.session.user = undefined
@@ -83,13 +131,27 @@ export const frontEndApis: ApiHandlers = {
131 return { user: ctx.session?.user, ...makeExp() }
132 },
133
86 - async change_pwd({ newPassword }, ctx) {
134 + async change_password({ newPassword }, ctx) {
135 + if (!newPassword) // clear text version
136 + return Error('missing parameters')
137 await updateAccount(await getCurrentUsername(ctx), account => {
138 account.password = newPassword
139 })
140 return true
141 },
142
143 + async change_srp({ salt, verifier }, ctx) {
144 + if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
145 + return ctx.status = 406
146 + if (!salt || !verifier)
147 + return Error('missing parameters')
148 + await updateAccount(await getCurrentUsername(ctx), account => {
149 + saveSrpInfo(account, salt, verifier)
150 + delete account.hashedPassword // remove leftovers
151 + })
152 + return true
153 + },
154 +
155 async extras_to_load() {
156 const css = []
157 for (const [k,plug] of Object.entries(plugins))
src/perm.ts
+37 -29
@@ -1,11 +1,12 @@
1 import fs from 'fs/promises'
2 import _ from 'lodash'
3 import yaml from 'yaml'
4 -import { hashPassword, verifyPassword } from './crypt'
4 +import { hashPassword } from './crypt'
5 import { setHidden, wantArray } from './misc'
6 import { watchLoad } from './watchLoad'
7 import Koa from 'koa'
8 -import { subscribeConfig } from './config'
8 +import { CFG_ALLOW_CLEAR_TEXT_LOGIN, getConfig, subscribeConfig } from './config'
9 +import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
10
11 let path = ''
12
@@ -13,6 +14,7 @@ interface Account {
14 user: string, // we'll have user in it, so we don't need to pass it separately
15 password?: string
16 hashedPassword?: string
17 + srp?: string
18 belongs?: string[]
19 }
20 interface Accounts { [username:string]: Account }
@@ -37,31 +39,47 @@ export async function getCurrentUsernameExpanded(ctx: Koa.Context) {
39 return ret
40 }
41
40 -export async function verifyLogin(user:string, password: string) {
41 - const acc = accounts[user]
42 - if (!acc) return
43 - const { hashedPassword: h } = acc
44 - return h && verifyPassword(h, password)
45 -}
46 -
42 export function getAccount(username:string) : Account {
43 return accounts[username]
44 }
45
46 +export function saveSrpInfo(account:Account, salt:string | bigint, verifier: string | bigint) {
47 + account.srp = String(salt) + '|' + String(verifier)
48 +}
49 +
50 +const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
51 +
52 type Changer = (account:Account)=> void | Promise<void>
52 -export async function updateAccount(username: string, changer:Changer) {
53 +export async function updateAccount(username: string, changer?:Changer) {
54 const account = getAccount(username)
54 - await changer(account)
55 + const was = JSON.stringify(account)
56 + await changer?.(account)
57 if (account.password) {
56 - account.hashedPassword = await hashPassword(account.password)
58 + console.debug('hashing password for', username)
59 + if (getConfig(CFG_ALLOW_CLEAR_TEXT_LOGIN))
60 + account.hashedPassword = await hashPassword(account.password)
61 + const res = await createVerifierAndSalt(srp6aNimbusRoutines, username, account.password)
62 + saveSrpInfo(account, res.s, res.v)
63 delete account.password
64 }
59 - saveAccountsAsap()
65 + else if (!account.srp && account.hashedPassword) {
66 + console.log('please reset password for account', username)
67 + process.exit(1)
68 + }
69 + account.belongs = wantArray(account.belongs).filter(b =>
70 + b in accounts // at this stage the group record may still be null if specified later in the file
71 + || console.error(`user ${username} belongs to non-existing ${b}`) )
72 + if (was !== JSON.stringify(account))
73 + saveAccountsAsap()
74 }
75
62 -const saveAccountsAsap = _.debounce(() =>
63 - fs.writeFile(path, yaml.stringify({ accounts })).catch(err =>
64 - console.error('Failed at saving accounts file, please ensure it is writable.', String(err))))
76 +let saving = false
77 +const saveAccountsAsap = _.debounce(() => {
78 + saving = true
79 + fs.writeFile(path, yaml.stringify({ accounts }, { lineWidth:1000 })) // we don't want big numbers to be folded
80 + .catch(err => console.error('Failed at saving accounts file, please ensure it is writable.', String(err)))
81 + .finally(()=> saving = false)
82 +})
83
84 let watcher: undefined | (()=>void)
85 subscribeConfig({ k:'accounts', defaultValue:'accounts.yaml' }, v => {
@@ -71,6 +89,7 @@ subscribeConfig({ k:'accounts', defaultValue:'accounts.yaml' }, v => {
89 if (typeof v !== 'string')
90 return console.error('bad type for accounts')
91 watcher = watchLoad(path = v, async data => {
92 + if (saving) return
93 const a = data?.accounts
94 if (!a)
95 return console.error('accounts file must contain "accounts" key')
@@ -81,21 +100,10 @@ subscribeConfig({ k:'accounts', defaultValue:'accounts.yaml' }, v => {
100 async function applyAccounts(newAccounts:Accounts) {
101 // we should validate content here
102 accounts = newAccounts
84 - let changed = false
103 await Promise.all(_.map(newAccounts, async (rec,k) => {
104 if (!rec) // an empty object in yaml is stored as null
87 - rec = accounts[k] = { user: '' }
105 + rec = accounts[k] = { user: k, srp:'' }
106 setHidden(rec, { user: k })
89 - rec.belongs = wantArray(rec.belongs).filter(b =>
90 - b in accounts // at this stage the group record may still be null if specified later in the file
91 - || console.error(`user ${k} belongs to non-existing ${b}`) )
92 - if (rec.password) {
93 - rec.hashedPassword = await hashPassword(rec.password)
94 - delete rec.password
95 - changed = true
96 - console.debug('hashing password for', k)
97 - }
107 + await updateAccount(k)
108 }))
99 - if (changed)
100 - await saveAccountsAsap()
109 }
todo.md
+1
@@ -1,4 +1,5 @@
1 # To do
2 +- fix: plugins folder is not found
3 - anti-csrf
4 - upload
5 - search and login dialogs should push to history so that mobile can use back button to close them