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