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