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"> | <script setup lang="ts"> | ||||||
| import { compareSync, hashSync } from 'bcryptjs'; | import { compare, hash } from 'bcryptjs'; | ||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import { useCopy } from '@/composable/copy'; | import { useCopy } from '@/composable/copy'; | ||||||
| 
 | 
 | ||||||
| const themeVars = useThemeVars(); | 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 input = ref(''); | ||||||
| const saltCount = ref(10); | const saltCount = ref(10); | ||||||
| const hashed = computed(() => hashSync(input.value, saltCount.value)); | initWatcher(hash, input, saltCount, hashState); | ||||||
| const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' }); |  | ||||||
| 
 | 
 | ||||||
|  | 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 compareString = ref(''); | ||||||
| const compareHash = ref(''); | const compareHash = ref(''); | ||||||
| const compareMatch = computed(() => compareSync(compareString.value, compareHash.value)); | initWatcher(compare, compareString, compareHash, compareState); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| @ -28,10 +176,19 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash | |||||||
|       mb-2 |       mb-2 | ||||||
|     /> |     /> | ||||||
|     <n-form-item label="Salt count: " label-placement="left" label-width="120"> |     <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> |     </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> |     <div mt-5 flex justify-center> | ||||||
|       <c-button @click="copy()"> |       <c-button @click="copy()"> | ||||||
| @ -48,21 +205,37 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash | |||||||
|       <n-form-item label="Your hash: " label-placement="left"> |       <n-form-item label="Your hash: " label-placement="left"> | ||||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text /> |         <c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text /> | ||||||
|       </n-form-item> |       </n-form-item> | ||||||
|       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | 
 | ||||||
|         <div class="compare-result" :class="{ positive: compareMatch }"> |       <n-progress :percentage="compareState.percentage" :show-indicator="false" /> | ||||||
|           {{ compareMatch ? 'Yes' : 'No' }} |       <div> | ||||||
|         </div> |         <c-input-text | ||||||
|       </n-form-item> |           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> |     </n-form> | ||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped> | <style lang="less"> | ||||||
| .compare-result { | .compare-result { | ||||||
|   color: v-bind('themeVars.errorColor'); |   &.negative { | ||||||
| 
 |     input#bcrypt-compare-result { | ||||||
|  |       color: v-bind('themeVars.errorColor'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   &.positive { |   &.positive { | ||||||
|     color: v-bind('themeVars.successColor'); |     input#bcrypt-compare-result { | ||||||
|  |       color: v-bind('themeVars.successColor'); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user