fix(bcrypt tool): Fix bcrypt error states and crashes (#1133)
This commit is contained in:
		
							parent
							
								
									e876d03608
								
							
						
					
					
						commit
						12d76e69a1
					
				| @ -1,18 +1,166 @@ | ||||
| <script setup lang="ts"> | ||||
| import { compareSync, hashSync } from 'bcryptjs'; | ||||
| import { compare, hash } from 'bcryptjs'; | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const themeVars = useThemeVars(); | ||||
| 
 | ||||
| interface ExecutionState<T> { | ||||
|   result: T | null | ||||
|   percentage: number | ||||
|   error: string | null | ||||
|   timeTakenMs: number | null | ||||
| } | ||||
| 
 | ||||
| const blankState = () => ({ result: null, percentage: 0, error: null, timeTakenMs: null }); | ||||
| 
 | ||||
| type Update<Result> = | ||||
|   | { | ||||
|     kind: 'progress' | ||||
|     progress: number | ||||
|   } | ||||
|   | { | ||||
|     kind: 'result' | ||||
|     result: Result | ||||
|     timeTakenMs: number | ||||
|   } | ||||
|   | { | ||||
|     kind: 'error' | ||||
|     message: string | ||||
|   }; | ||||
| 
 | ||||
| const TIMEOUT_SECONDS = 10; | ||||
| 
 | ||||
| class TimedOutError extends Error { | ||||
|   name = 'TimedOutError'; | ||||
| } | ||||
| class InvalidatedError extends Error { | ||||
|   name = 'InvalidatedError'; | ||||
| } | ||||
| 
 | ||||
| // generic type for the callback versions of bcryptjs's `hash` and `compare` | ||||
| type BcryptFn<Param, Result> = ( | ||||
|   arg1: string, | ||||
|   arg2: Param, | ||||
|   callback: (err: Error | null, hash: Result) => void, | ||||
|   progressCallback: (percent: number) => void, | ||||
| ) => void; | ||||
| 
 | ||||
| async function* runWithProgress<Param, Result>( | ||||
|   fn: BcryptFn<Param, Result>, | ||||
|   arg1: string, | ||||
|   arg2: Param, | ||||
|   controller: AbortController, | ||||
| ): AsyncGenerator<Update<Result>, undefined, undefined> { | ||||
|   let res = (_: Update<Result>) => {}; | ||||
|   const nextPromise = () => | ||||
|     new Promise<Update<Result>>((resolve) => { | ||||
|       res = resolve; | ||||
|     }); | ||||
|   const promises = [nextPromise()]; | ||||
|   const nextValue = (value: Update<Result>) => { | ||||
|     res(value); | ||||
|     promises.push(nextPromise()); | ||||
|   }; | ||||
| 
 | ||||
|   const start = Date.now(); | ||||
| 
 | ||||
|   fn( | ||||
|     arg1, | ||||
|     arg2, | ||||
|     (err, result) => { | ||||
|       nextValue( | ||||
|         err == null | ||||
|           ? { kind: 'result', result, timeTakenMs: Date.now() - start } | ||||
|           : { kind: 'error', message: err.message }, | ||||
|       ); | ||||
|     }, | ||||
|     (progress) => { | ||||
|       if (controller.signal.aborted) { | ||||
|         nextValue({ kind: 'progress', progress: 0 }); | ||||
|         if (controller.signal.reason instanceof TimedOutError) { | ||||
|           nextValue({ kind: 'error', message: controller.signal.reason.message }); | ||||
|         } | ||||
| 
 | ||||
|         // throw inside callback to cancel execution of hashing/comparing | ||||
|         throw controller.signal.reason; | ||||
|       } | ||||
|       else { | ||||
|         nextValue({ kind: 'progress', progress }); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   setTimeout(() => { | ||||
|     controller.abort(new TimedOutError(`Timed out after ${TIMEOUT_SECONDS} seconds`)); | ||||
|   }, TIMEOUT_SECONDS * 1000); | ||||
| 
 | ||||
|   for await (const value of promises) { | ||||
|     yield value; | ||||
| 
 | ||||
|     if (value.kind === 'result' || value.kind === 'error') { | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| async function exec<Param, Result>( | ||||
|   fn: BcryptFn<Param, Result>, | ||||
|   arg1: string | null, | ||||
|   arg2: Param | null, | ||||
|   controller: AbortController, | ||||
|   state: ExecutionState<Result>, | ||||
| ) { | ||||
|   if (arg1 == null || arg2 == null) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   for await (const value of runWithProgress(fn, arg1, arg2, controller)) { | ||||
|     switch (value.kind) { | ||||
|       case 'progress': { | ||||
|         state.percentage = value.progress * 100; | ||||
|         break; | ||||
|       } | ||||
|       case 'result': { | ||||
|         state.result = value.result; | ||||
|         state.timeTakenMs = value.timeTakenMs; | ||||
|         break; | ||||
|       } | ||||
|       case 'error': { | ||||
|         state.error = value.message; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function initWatcher<Param, Result>( | ||||
|   fn: BcryptFn<Param, Result>, | ||||
|   input1: Ref<string | null>, | ||||
|   input2: Ref<Param | null>, | ||||
|   state: Ref<ExecutionState<Result>>, | ||||
| ) { | ||||
|   let controller = new AbortController(); | ||||
|   watch([input1, input2], ([input1, input2]) => { | ||||
|     controller.abort(new InvalidatedError()); | ||||
|     controller = new AbortController(); | ||||
|     state.value = blankState(); | ||||
|     exec(fn, input1, input2, controller, state.value); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| const hashState = ref<ExecutionState<string>>(blankState()); | ||||
| const input = ref(''); | ||||
| const saltCount = ref(10); | ||||
| const hashed = computed(() => hashSync(input.value, saltCount.value)); | ||||
| const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' }); | ||||
| initWatcher(hash, input, saltCount, hashState); | ||||
| 
 | ||||
| const source = computed(() => hashState.value.result ?? ''); | ||||
| const { copy } = useCopy({ source, text: 'Hashed string copied to the clipboard' }); | ||||
| 
 | ||||
| const compareState = ref<ExecutionState<boolean>>(blankState()); | ||||
| const compareString = ref(''); | ||||
| const compareHash = ref(''); | ||||
| const compareMatch = computed(() => compareSync(compareString.value, compareHash.value)); | ||||
| initWatcher(compare, compareString, compareHash, compareState); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
| @ -28,10 +176,19 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash | ||||
|       mb-2 | ||||
|     /> | ||||
|     <n-form-item label="Salt count: " label-placement="left" label-width="120"> | ||||
|       <n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="100" :min="0" w-full /> | ||||
|       <n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="20" :min="0" w-full /> | ||||
|     </n-form-item> | ||||
| 
 | ||||
|     <c-input-text :value="hashed" readonly text-center /> | ||||
|     <n-progress :percentage="hashState.percentage" :show-indicator="false" /> | ||||
|     <c-input-text | ||||
|       :value="hashState.result ?? undefined" | ||||
|       :placeholder="hashState.error ?? 'Hashed string'" | ||||
|       readonly | ||||
|       text-center | ||||
|     /> | ||||
|     <div mt-1 h-3 op-60> | ||||
|       {{ hashState.timeTakenMs == null ? '' : `Hashed in ${hashState.timeTakenMs}\xa0ms` }} | ||||
|     </div> | ||||
| 
 | ||||
|     <div mt-5 flex justify-center> | ||||
|       <c-button @click="copy()"> | ||||
| @ -48,21 +205,37 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash | ||||
|       <n-form-item label="Your hash: " label-placement="left"> | ||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | ||||
|         <div class="compare-result" :class="{ positive: compareMatch }"> | ||||
|           {{ compareMatch ? 'Yes' : 'No' }} | ||||
| 
 | ||||
|       <n-progress :percentage="compareState.percentage" :show-indicator="false" /> | ||||
|       <div> | ||||
|         <c-input-text | ||||
|           id="bcrypt-compare-result" | ||||
|           :value="compareState.result == null ? undefined : compareState.result ? 'Matched' : 'No match'" | ||||
|           :placeholder="compareState.error ?? 'Comparison result'" | ||||
|           readonly | ||||
|           text-center | ||||
|           class="compare-result" | ||||
|           :class="compareState.result == null ? undefined : compareState.result ? 'positive' : 'negative'" | ||||
|         /> | ||||
|       </div> | ||||
|       <div mb-1 mt-1 h-3 op-60> | ||||
|         {{ compareState.timeTakenMs == null ? '' : `Compared in ${compareState.timeTakenMs}\xa0ms` }} | ||||
|       </div> | ||||
|       </n-form-item> | ||||
|     </n-form> | ||||
|   </c-card> | ||||
| </template> | ||||
| 
 | ||||
| <style lang="less" scoped> | ||||
| <style lang="less"> | ||||
| .compare-result { | ||||
|   &.negative { | ||||
|     input#bcrypt-compare-result { | ||||
|       color: v-bind('themeVars.errorColor'); | ||||
| 
 | ||||
|     } | ||||
|   } | ||||
|   &.positive { | ||||
|     input#bcrypt-compare-result { | ||||
|       color: v-bind('themeVars.successColor'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user