better code: removed legacy
Massimo Melina committed
Jan 25, 2024 at 15:52 UTC
fd7832232aec991fe4f1c6fbc47b339c87ce1279
11 files changed
+22
-192
admin/src/AccountForm.ts
+1
-5
@@ -116,9 +116,5 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
116
export async function apiNewPassword(username: string, password: string) {
117
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
118
const res = await createVerifierAndSalt(srp6aNimbusRoutines, username, password)
119
- return apiCall('change_srp_others', { username, salt: String(res.s), verifier: String(res.v) }).catch(e => {
120
- if (e.code !== HTTP_NOT_ACCEPTABLE) // server doesn't support clear text authentication
121
- throw e
122
- return apiCall('change_password_others', { username, newPassword: password }) // unencrypted version
123
- })
119
+ return apiCall('change_srp', { username, salt: String(res.s), verifier: String(res.v) })
120
}
frontend/src/UserPanel.ts
+1
-5
@@ -35,11 +35,7 @@ export default function showUserPanel() {
35
const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
36
const res = await createVerifierAndSalt(srp6aNimbusRoutines, snap.username, pwd)
37
try {
38
- await apiCall('change_srp', { salt: String(res.s), verifier: String(res.v) }, { modal: working }).catch(e => {
39
- if (e.code !== HTTP_NOT_ACCEPTABLE) // server doesn't support clear text authentication
40
- throw e
41
- return apiCall('change_password', { newPassword: pwd }, { modal: working }) // unencrypted version
42
- })
38
+ await apiCall('change_my_srp', { salt: String(res.s), verifier: String(res.v) }, { modal: working })
39
return alertDialog(t('password_changed', "Password changed"))
40
}
41
catch(e) {
src/api.accounts.ts
+2
-9
@@ -1,9 +1,8 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { changePasswordHelper, changeSrpHelper } from './api.helpers'
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
import { Account, accountCanLoginAdmin, accountHasPassword, accountsConfig, addAccount, delAccount, getAccount,
6
- setAccount } from './perm'
5
+ setAccount, changeSrpHelper } from './perm'
6
import _ from 'lodash'
7
import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_FOUND } from './const'
8
import { getCurrentUsername, invalidSessions } from './auth'
@@ -65,13 +64,7 @@ export default {
64
return {}
65
},
66
68
- async change_password_others({ username, newPassword }) {
69
- const a = getAccount(username)
70
- return a ? changePasswordHelper(a, newPassword)
71
- : new ApiError(HTTP_NOT_FOUND)
72
- },
73
-
74
- async change_srp_others({ username, salt, verifier }) {
67
+ async change_srp({ username, salt, verifier }) {
68
const a = getAccount(username)
69
return a ? changeSrpHelper(a, salt, verifier)
70
: new ApiError(HTTP_NOT_FOUND)
src/api.auth.ts
+3
-28
@@ -1,12 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Account, accountCanLogin, getAccount, getFromAccount } from './perm'
4
-import { verifyPassword } from './crypt'
3
+import { Account, accountCanLogin, changeSrpHelper, getAccount, getFromAccount } from './perm'
4
import { ApiError, ApiHandler } from './apiMiddleware'
5
import { SRPServerSessionStep1 } from 'tssrp6a'
7
-import { ADMIN_URI, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_NOT_ACCEPTABLE, HTTP_CONFLICT,
8
- HTTP_NOT_FOUND } from './const'
9
-import { changeSrpHelper, changePasswordHelper } from './api.helpers'
6
+import { ADMIN_URI, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_FOUND } from './const'
7
import { ctxAdminAccess } from './adminApis'
8
import { sessionDuration } from './middlewares'
9
import { getCurrentUsername, loggedIn, srpStep1 } from './auth'
@@ -20,22 +17,6 @@ function makeExp() {
17
: { exp: new Date(Date.now() + sessionDuration.compiled()) }
18
}
19
23
-export const login: ApiHandler = async ({ username, password }, ctx) => {
24
- if (!username || !password) // some validation
25
- return new ApiError(HTTP_BAD_REQUEST)
26
- const account = getAccount(username)
27
- if (!account || !accountCanLogin(account))
28
- return new ApiError(HTTP_UNAUTHORIZED)
29
- if (!account.hashed_password)
30
- return new ApiError(HTTP_NOT_ACCEPTABLE)
31
- if (!await verifyPassword(account.hashed_password, password))
32
- return new ApiError(HTTP_UNAUTHORIZED)
33
- if (!ctx.session)
34
- return new ApiError(HTTP_SERVER_ERROR)
35
- await loggedIn(ctx, username)
36
- return { ...makeExp(), redirect: account.redirect }
37
-}
38
-
20
export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
21
if (!username)
22
return new ApiError(HTTP_BAD_REQUEST)
@@ -106,13 +87,7 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
87
}
88
}
89
109
-export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
110
- const a = ctx.state.account
111
- return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
112
- : changePasswordHelper(a, newPassword)
113
-}
114
-
115
-export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
90
+export const change_my_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
91
const a = ctx.state.account
92
return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
93
: changeSrpHelper(a, salt, verifier)
src/api.helpers.ts
deleted
-26
@@ -1,26 +0,0 @@
1
-// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
-import { Account, allowClearTextLogin, saveSrpInfo, updateAccount } from './perm'
4
-import { ApiError } from './apiMiddleware'
5
-import { HTTP_BAD_REQUEST, HTTP_NOT_ACCEPTABLE } from './const'
6
-
7
-export async function changePasswordHelper(account: Account, newPassword: string) {
8
- if (!newPassword) // clear text version
9
- return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')
10
- await updateAccount(account, account => {
11
- account.password = newPassword
12
- })
13
- return {}
14
-}
15
-
16
-export async function changeSrpHelper(account: Account, salt: string, verifier: string) {
17
- if (allowClearTextLogin.get())
18
- return new ApiError(HTTP_NOT_ACCEPTABLE)
19
- if (!salt || !verifier)
20
- return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')
21
- await updateAccount(account, account => {
22
- saveSrpInfo(account, salt, verifier)
23
- delete account.hashed_password // remove leftovers
24
- })
25
- return {}
26
-}
src/crypt.ts
deleted
-16
@@ -1,16 +0,0 @@
1
-// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
-// simple wrapper
4
-// @ts-ignore
5
-import { pbkdf2, pbkdf2Verify } from "./pbkdf2"
6
-import assert from 'assert'
7
-
8
-export async function hashPassword(s: string) {
9
- return 'p2:' + await pbkdf2(s)
10
-}
11
-
12
-export async function verifyPassword(hashed: string, given: string) {
13
- const i = hashed.indexOf(':')
14
- assert(i>0, 'bad hashed')
15
- return await pbkdf2Verify(hashed.slice(i+1), given) // for the time being we totally ignore the "method" part
16
-}
src/frontEndApis.ts
+2
-4
@@ -6,10 +6,8 @@ import * as api_auth from './api.auth'
6
import events from './events'
7
import Koa from 'koa'
8
import { dirTraversal, isValidFileName } from './util-files'
9
-import {
10
- HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FAILED_DEPENDENCY, HTTP_FORBIDDEN,
11
- HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED
12
-} from './const'
9
+import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FAILED_DEPENDENCY, HTTP_FORBIDDEN,
10
+ HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED } from './const'
11
import { hasPermission, statusCodeForMissingPerm, urlToNode } from './vfs'
12
import { mkdir, rename, rm } from 'fs/promises'
13
import { basename, dirname, join } from 'path'
src/middlewares.ts
+1
-1
@@ -118,7 +118,7 @@ export const prepareState: Koa.Middleware = async (ctx, next) => {
118
const a = await srpCheck(u, p)
119
if (a) {
120
ctx.session!.username = a.username
121
- ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length-1))
121
+ ctx.redirect(ctx.originalUrl.slice(0, -ctx.querystring.length-1)) // redirect to hide credentials
122
}
123
return a
124
}
src/pbkdf2.ts
deleted
-83
@@ -1,83 +0,0 @@
1
-// @ts-nocheck
2
-import { webcrypto as crypto } from "node:crypto";
3
-export { pbkdf2, pbkdf2Verify }
4
-
5
-// FROM https://gist.github.com/chrisveness/770ee96945ec12ac84f134bf538d89fb
6
-
7
-/**
8
- * Returns PBKDF2 derived key from supplied password.
9
- *
10
- * Stored key can subsequently be used to verify that a password matches the original password used
11
- * to derive the key, using pbkdf2Verify().
12
- *
13
- * @param {String} password - Password to be hashed using key derivation function.
14
- * @param {Number} [iterations=1e6] - Number of iterations of HMAC function to apply.
15
- * @returns {String} Derived key as base64 string.
16
- *
17
- * @example
18
- * const key = await pbkdf2('pāşšŵōřđ'); // eg 'djAxBRKXWNWPyXgpKWHld8SWJA9CQFmLyMbNet7Rle5RLKJAkBCllLfM6tPFa7bAis0lSTiB'
19
- */
20
-async function pbkdf2(password, iterations=1e6) {
21
- const pwUtf8 = new TextEncoder().encode(password); // encode pw as UTF-8
22
- const pwKey = await crypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveBits']); // create pw key
23
-
24
- const saltUint8 = crypto.getRandomValues(new Uint8Array(16)); // get random salt
25
-
26
- const params = { name: 'PBKDF2', hash: 'SHA-256', salt: saltUint8, iterations: iterations }; // pbkdf2 params
27
- const keyBuffer = await crypto.subtle.deriveBits(params, pwKey, 256); // derive key
28
-
29
- const keyArray = Array.from(new Uint8Array(keyBuffer)); // key as byte array
30
-
31
- const saltArray = Array.from(new Uint8Array(saltUint8)); // salt as byte array
32
-
33
- const iterHex = ('000000'+iterations.toString(16)).slice(-6); // iter’n count as hex
34
- const iterArray = iterHex.match(/.{2}/g).map(byte => parseInt(byte, 16)); // iter’ns as byte array
35
-
36
- const compositeArray = [].concat(saltArray, iterArray, keyArray); // combined array
37
- const compositeStr = compositeArray.map(byte => String.fromCharCode(byte)).join(''); // combined as string
38
- // encode as base64
39
- return btoa('v01' + compositeStr); // return composite key
40
-}
41
-
42
-
43
-/**
44
- * Verifies whether the supplied password matches the password previously used to generate the key.
45
- *
46
- * @param {String} key - Key previously generated with pbkdf2().
47
- * @param {String} password - Password to be matched against previously derived key.
48
- * @returns {boolean} Whether password matches key.
49
- *
50
- * @example
51
- * const match = await pbkdf2Verify(key, 'pāşšŵōřđ'); // true
52
- */
53
-async function pbkdf2Verify(key, password) {
54
- let compositeStr = null; // composite key is salt, iteration count, and derived key
55
- try { compositeStr = atob(key); } catch (e) { throw new Error ('Invalid key'); } // decode from base64
56
-
57
- const version = compositeStr.slice(0, 3); // 3 bytes
58
- const saltStr = compositeStr.slice(3, 19); // 16 bytes (128 bits)
59
- const iterStr = compositeStr.slice(19, 22); // 3 bytes
60
- const keyStr = compositeStr.slice(22, 54); // 32 bytes (256 bits)
61
-
62
- if (version !== 'v01') throw new Error('Invalid key');
63
-
64
- // -- recover salt & iterations from stored (composite) key
65
-
66
- const saltUint8 = new Uint8Array(saltStr.match(/./g).map(ch => ch.charCodeAt(0))); // salt as Uint8Array
67
- // note: cannot use TextEncoder().encode(saltStr) as it generates UTF-8
68
-
69
- const iterHex = iterStr.match(/./g).map(ch => ch.charCodeAt(0).toString(16)).join(''); // iter’n count as hex
70
- const iterations = parseInt(iterHex, 16); // iter’ns
71
-
72
- // -- generate new key from stored salt & iterations and supplied password
73
-
74
- const pwUtf8 = new TextEncoder().encode(password); // encode pw as UTF-8
75
- const pwKey = await crypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveBits']); // create pw key
76
-
77
- const params = { name: 'PBKDF2', hash: 'SHA-256', salt: saltUint8, iterations: iterations }; // pbkdf params
78
- const keyBuffer = await crypto.subtle.deriveBits(params, pwKey, 256); // derive key
79
- const keyArray = Array.from(new Uint8Array(keyBuffer)); // key as byte array
80
- const keyStrNew = keyArray.map(byte => String.fromCharCode(byte)).join(''); // key as string
81
-
82
- return keyStrNew === keyStr; // test if newly generated key matches stored key
83
-}
src/perm.ts
+12
-13
@@ -1,17 +1,15 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import _ from 'lodash'
4
-import { hashPassword } from './crypt'
5
-import { objRenameKey, setHidden, wantArray } from './misc'
6
-import Koa from 'koa'
4
+import { HTTP_BAD_REQUEST, objRenameKey, setHidden, wantArray } from './misc'
5
import { defineConfig, saveConfigAsap } from './config'
6
import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
7
import events from './events'
8
+import { ApiError } from './apiMiddleware'
9
10
export interface Account {
11
username: string, // we keep username property (hidden) so we don't need to pass it separately
12
password?: string
14
- hashed_password?: string
13
srp?: string
14
belongs?: string[]
15
ignore_limits?: boolean
@@ -48,8 +46,6 @@ export function saveSrpInfo(account:Account, salt:string | bigint, verifier: str
46
account.srp = String(salt) + '|' + String(verifier)
47
}
48
51
-export const allowClearTextLogin = defineConfig('allow_clear_text_login', false)
52
-
49
const createAdminConfig = defineConfig('create-admin', '')
50
createAdminConfig.sub(v => {
51
if (!v) return
@@ -73,16 +69,10 @@ export async function updateAccount(account: Account, changer?:Changer) {
69
const { username } = account
70
if (account.password) {
71
console.debug('hashing password for', username)
76
- if (allowClearTextLogin.get())
77
- account.hashed_password = await hashPassword(account.password)
72
const res = await createVerifierAndSalt(srp6aNimbusRoutines, username, account.password)
73
saveSrpInfo(account, res.s, res.v)
74
delete account.password
75
}
82
- else if (!account.srp && account.hashed_password) {
83
- console.log('please reset password for account', username)
84
- process.exit(1)
85
- }
76
if (account.belongs) {
77
account.belongs = wantArray(account.belongs)
78
_.remove(account.belongs, b => {
@@ -190,7 +180,7 @@ export function getFromAccount<T=any>(account: Account | string, getter:(a:Accou
180
}
181
182
export function accountHasPassword(account: Account) {
193
- return Boolean(account.password || account.hashed_password || account.srp)
183
+ return Boolean(account.password || account.srp)
184
}
185
186
export function accountCanLogin(account: Account) {
@@ -204,3 +194,12 @@ function allDisabled(account: Account): boolean {
194
export function accountCanLoginAdmin(account: Account) {
195
return accountCanLogin(account) && Boolean(getFromAccount(account, a => a.admin))
196
}
197
+
198
+
199
+export async function changeSrpHelper(account: Account, salt: string, verifier: string) {
200
+ if (!salt || !verifier)
201
+ return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')
202
+ await updateAccount(account, account =>
203
+ saveSrpInfo(account, salt, verifier) )
204
+ return {}
205
+}
\ No newline at end of file
tests/test.ts
-2
@@ -101,7 +101,6 @@ describe('basics', () => {
101
it('zip.partial.end', req('/f1/f2/?get=zip', { re:/^6/, length:10 }, { headers: { Range: 'bytes=-10' } }) )
102
it('zip.alfa is forbidden', req('/protectFromAbove/child/?get=zip&list=alfa.txt*renamed', { empty: true, length:118 }, { method:'HEAD' }))
103
it('zip.cantReadPage', req('/cantReadPage/?get=zip', { length: 120 }, { method:'HEAD' }))
104
- it('login', reqApi('login', { username, password }, 406)) // by default, we don't support clear-text login
104
105
it('referer', req('/f1/page/gpl.png', 403, {
106
headers: { Referer: 'https://some-website.com/try-to-trick/x.com/' }
@@ -118,7 +117,6 @@ describe('basics', () => {
117
describe('accounts', () => {
118
const username = 'test-Add'
119
it('accounts.add', reqApi('add_account', { username }, res => res?.username === username.toLowerCase()))
121
- it('account.password', reqApi('change_password_others', { username, newPassword: password }, 200))
120
it('accounts.remove', reqApi('del_account', { username }, 200))
121
})
122