Merge 1793eb5396 into 07eea0f484
				
					
				
			This commit is contained in:
		
						commit
						e8ec4b2b7f
					
				| @ -1,24 +1,249 @@ | ||||
| <script setup lang="ts"> | ||||
| // Import cryptographic algorithms from crypto-js and Vue composition API | ||||
| import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js'; | ||||
| import { computedCatch } from '@/composable/computed/catchedComputed'; | ||||
| import { ref, watch } from 'vue'; | ||||
| 
 | ||||
| const algos = { AES, TripleDES, Rabbit, RC4 }; | ||||
| // Available modes for AES | ||||
| const aesModes = [ | ||||
|   { label: 'CBC (CryptoJS)', value: 'CBC' }, | ||||
|   { label: 'GCM (WebCrypto)', value: 'GCM' }, | ||||
| ]; | ||||
| 
 | ||||
| // Detect if Web Crypto API is available and supports AES-GCM | ||||
| const canUseWebCrypto = typeof window !== 'undefined' | ||||
|   && window.crypto?.subtle | ||||
|   && typeof window.crypto.subtle.encrypt === 'function'; | ||||
| 
 | ||||
| // Functions for AES-GCM using Web Crypto API | ||||
| async function webCryptoEncrypt(algo: 'AES-GCM', text: string, secret: string, ivHex?: string) { | ||||
|   if (!canUseWebCrypto) { | ||||
|     throw new Error('Web Crypto API not available'); | ||||
|   } | ||||
|   // Encode the plaintext and secret key | ||||
|   const encText = new TextEncoder().encode(text); | ||||
|   const encSecret = new TextEncoder().encode(secret.padEnd(32, ' ')).slice(0, 32); | ||||
|   let iv: Uint8Array; | ||||
|   // If IV is provided and valid, use it; otherwise, generate a random IV | ||||
|   if (ivHex && /^[0-9a-fA-F]{24}$/.test(ivHex)) { | ||||
|     iv = new Uint8Array(ivHex.match(/.{2}/g)!.map(h => Number.parseInt(h, 16))); | ||||
|   } | ||||
|   else { | ||||
|     iv = crypto.getRandomValues(new Uint8Array(12)); | ||||
|   } | ||||
|   // Import the secret key for AES-GCM | ||||
|   const key = await crypto.subtle.importKey( | ||||
|     'raw', | ||||
|     encSecret, | ||||
|     { name: algo, length: 256 }, | ||||
|     false, | ||||
|     ['encrypt'], | ||||
|   ); | ||||
|   const params: AesGcmParams = { | ||||
|     name: algo, | ||||
|     iv, | ||||
|     tagLength: 128, | ||||
|   }; | ||||
|   // Encrypt the plaintext using AES-GCM | ||||
|   const ciphertext = await crypto.subtle.encrypt(params, key, encText); | ||||
|   // Concatenate IV and ciphertext for easier decryption | ||||
|   const result = btoa(String.fromCharCode(...iv, ...new Uint8Array(ciphertext))); | ||||
|   return { result, iv: Array.from(iv).map(b => b.toString(16).padStart(2, '0')).join('') }; | ||||
| }; | ||||
| 
 | ||||
| async function webCryptoDecrypt(algo: 'AES-GCM', b64: string, secret: string) { | ||||
|   if (!canUseWebCrypto) { | ||||
|     throw new Error('Web Crypto API not available'); | ||||
|   } | ||||
|   try { | ||||
|     // Decode base64 and extract IV and ciphertext | ||||
|     const data = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); | ||||
|     const iv = data.slice(0, 12); // IV is embedded in the encrypted text | ||||
|     const ciphertext = data.slice(12); | ||||
|     // Prepare the secret key | ||||
|     const encSecret = new TextEncoder().encode(secret.padEnd(32, ' ')).slice(0, 32); | ||||
|     const key = await crypto.subtle.importKey( | ||||
|       'raw', | ||||
|       encSecret, | ||||
|       { name: algo, length: 256 }, | ||||
|       false, | ||||
|       ['decrypt'], | ||||
|     ); | ||||
|     const params: AesGcmParams = { | ||||
|       name: algo, | ||||
|       iv, | ||||
|       tagLength: 128, | ||||
|     }; | ||||
|     // Decrypt the ciphertext | ||||
|     const plain = await crypto.subtle.decrypt(params, key, ciphertext); | ||||
|     return new TextDecoder().decode(plain); | ||||
|   } | ||||
|   catch { | ||||
|     throw new Error('Unable to decrypt your text'); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // Available algorithms (AES unified) | ||||
| const algos = { | ||||
|   AES: { | ||||
|     modes: ['CBC', ...(canUseWebCrypto ? ['GCM'] : [])], | ||||
|     encrypt: (mode: string, text: string, secret: string) => { | ||||
|       // AES-CBC encryption using CryptoJS | ||||
|       if (mode === 'CBC') { | ||||
|         return { | ||||
|           toString: () => AES.encrypt(text, secret).toString(), | ||||
|         }; | ||||
|       } | ||||
|       // AES-GCM encryption using Web Crypto API | ||||
|       if (mode === 'GCM' && canUseWebCrypto) { | ||||
|         return { | ||||
|           toString: () => '', | ||||
|           _async: () => webCryptoEncrypt('AES-GCM', text, secret), | ||||
|         }; | ||||
|       } | ||||
|       return { toString: () => '' }; | ||||
|     }, | ||||
|     decrypt: (mode: string, b64: string, secret: string) => { | ||||
|       // AES-CBC decryption using CryptoJS | ||||
|       if (mode === 'CBC') { | ||||
|         return { | ||||
|           toString: (encoding: any) => AES.decrypt(b64, secret).toString(encoding), | ||||
|         }; | ||||
|       } | ||||
|       // AES-GCM decryption using Web Crypto API | ||||
|       if (mode === 'GCM' && canUseWebCrypto) { | ||||
|         return { | ||||
|           toString: () => '', | ||||
|           _async: () => webCryptoDecrypt('AES-GCM', b64, secret), | ||||
|         }; | ||||
|       } | ||||
|       return { toString: () => '' }; | ||||
|     }, | ||||
|   }, | ||||
|   TripleDES, | ||||
|   Rabbit, | ||||
|   RC4, | ||||
| }; | ||||
| 
 | ||||
| // Reactive state for encryption inputs and outputs | ||||
| const cypherInput = ref('Lorem ipsum dolor sit amet'); | ||||
| const cypherAlgo = ref<keyof typeof algos>('AES'); | ||||
| const cypherAesMode = ref('CBC'); | ||||
| const cypherSecret = ref('my secret key'); | ||||
| const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString()); | ||||
| const cypherOutput = ref(''); | ||||
| const cypherIV = ref<string>(generateRandomIVHex(12)); // Default random IV | ||||
| 
 | ||||
| // Reactive state for decryption inputs and outputs | ||||
| const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs'); | ||||
| const decryptAlgo = ref<keyof typeof algos>('AES'); | ||||
| const decryptAesMode = ref('CBC'); | ||||
| const decryptSecret = ref('my secret key'); | ||||
| const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), { | ||||
|   defaultValue: '', | ||||
|   defaultErrorMessage: 'Unable to decrypt your text', | ||||
| const decryptOutput = ref(''); | ||||
| const decryptError = ref(''); | ||||
| 
 | ||||
| // Utility to generate random IV in hex format | ||||
| function generateRandomIVHex(length = 12) { | ||||
|   return Array.from(crypto.getRandomValues(new Uint8Array(length))) | ||||
|     .map(b => b.toString(16).padStart(2, '0')).join(''); | ||||
| } | ||||
| 
 | ||||
| // Watchers to handle both sync and async algorithms for encryption | ||||
| watch([cypherInput, cypherSecret, cypherAlgo, cypherAesMode, cypherIV], async () => { | ||||
|   // If AES is selected, handle modes | ||||
|   if (cypherAlgo.value === 'AES') { | ||||
|     const mode = cypherAesMode.value; | ||||
|     const algo = algos.AES; | ||||
|     if (mode === 'GCM') { | ||||
|       cypherOutput.value = 'Encrypting...'; | ||||
|       try { | ||||
|         const ivHex = cypherIV.value; | ||||
|         // Validate IV for GCM mode | ||||
|         if (!/^[0-9a-fA-F]{24}$/.test(ivHex)) { | ||||
|           cypherOutput.value = 'IV must be 24 hex characters (12 bytes)'; | ||||
|           return; | ||||
|         } | ||||
|         // Pass IV to webCryptoEncrypt | ||||
|         const encResult = await webCryptoEncrypt('AES-GCM', cypherInput.value, cypherSecret.value, ivHex); | ||||
|         if (typeof encResult === 'object' && encResult.result && encResult.iv) { | ||||
|           cypherOutput.value = encResult.result; | ||||
|         } | ||||
|         else if (typeof encResult === 'string') { | ||||
|           cypherOutput.value = encResult; | ||||
|         } | ||||
|       } | ||||
|       catch { | ||||
|         cypherOutput.value = 'Encryption error'; | ||||
|       } | ||||
|     } | ||||
|     else { | ||||
|       // AES-CBC encryption | ||||
|       cypherOutput.value = algo.encrypt(mode, cypherInput.value, cypherSecret.value).toString(); | ||||
|     } | ||||
|   } | ||||
|   else { | ||||
|     // Non-AES algorithms | ||||
|     const algo = algos[cypherAlgo.value]; | ||||
|     cypherOutput.value = algo.encrypt(cypherInput.value, cypherSecret.value).toString(); | ||||
|   } | ||||
| }, { immediate: true }); | ||||
| 
 | ||||
| // Watcher to generate a new IV if switching to GCM mode and IV is invalid | ||||
| watch([cypherAesMode, cypherAlgo], () => { | ||||
|   if ( | ||||
|     cypherAlgo.value === 'AES' | ||||
|     && cypherAesMode.value === 'GCM' | ||||
|     && (!/^[0-9a-fA-F]{24}$/.test(cypherIV.value)) | ||||
|   ) { | ||||
|     cypherIV.value = generateRandomIVHex(12); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| // Watchers to handle both sync and async algorithms for decryption | ||||
| watch([decryptInput, decryptSecret, decryptAlgo, decryptAesMode], async () => { | ||||
|   decryptError.value = ''; | ||||
|   if (decryptAlgo.value === 'AES') { | ||||
|     const mode = decryptAesMode.value; | ||||
|     const algo = algos.AES; | ||||
|     if (mode === 'GCM') { | ||||
|       decryptOutput.value = 'Decrypting...'; | ||||
|       try { | ||||
|         const decryptionResult = algo.decrypt(mode, decryptInput.value, decryptSecret.value); | ||||
|         if (decryptionResult && typeof decryptionResult._async === 'function') { | ||||
|           decryptOutput.value = await decryptionResult._async(); | ||||
|         } | ||||
|         else { | ||||
|           throw new Error('Invalid decryption result or unsupported mode.'); | ||||
|         } | ||||
|       } | ||||
|       catch (e: any) { | ||||
|         decryptOutput.value = ''; | ||||
|         decryptError.value = e?.message || 'Unable to decrypt your text'; | ||||
|       } | ||||
|     } | ||||
|     else { | ||||
|       try { | ||||
|         decryptOutput.value = algo.decrypt(mode, decryptInput.value, decryptSecret.value).toString(enc.Utf8); | ||||
|       } | ||||
|       catch { | ||||
|         decryptOutput.value = ''; | ||||
|         decryptError.value = 'Unable to decrypt your text'; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   else { | ||||
|     const algo = algos[decryptAlgo.value]; | ||||
|     try { | ||||
|       decryptOutput.value = algo.decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8); | ||||
|     } | ||||
|     catch { | ||||
|       decryptOutput.value = ''; | ||||
|       decryptError.value = 'Unable to decrypt your text'; | ||||
|     } | ||||
|   } | ||||
| }, { immediate: true }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <!-- Encryption card UI --> | ||||
|   <c-card title="Encrypt"> | ||||
|     <div flex gap-3> | ||||
|       <c-input-text | ||||
| @ -30,12 +255,30 @@ const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.valu | ||||
|       /> | ||||
|       <div flex flex-1 flex-col gap-2> | ||||
|         <c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text /> | ||||
| 
 | ||||
|         <c-select | ||||
|           v-model:value="cypherAlgo" | ||||
|           label="Encryption algorithm:" | ||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|         /> | ||||
|         <!-- Show modes only if AES is selected --> | ||||
|         <c-select | ||||
|           v-if="cypherAlgo === 'AES'" | ||||
|           v-model:value="cypherAesMode" | ||||
|           label="AES mode:" | ||||
|           :options="aesModes" | ||||
|         /> | ||||
|         <!-- Editable IV only for GCM, below the mode selector --> | ||||
|         <c-input-text | ||||
|           v-if="cypherAlgo === 'AES' && cypherAesMode === 'GCM'" | ||||
|           v-model:value="cypherIV" | ||||
|           label="IV (hex):" | ||||
|           maxlength="24" | ||||
|           monospace | ||||
|           clearable | ||||
|           placeholder="24 hex chars" | ||||
|           suffix-icon="i-carbon-renew" | ||||
|           @click-suffix="cypherIV = generateRandomIVHex(12)" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|     <c-input-text | ||||
| @ -46,6 +289,7 @@ const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.valu | ||||
|       multiline monospace readonly autosize mt-5 | ||||
|     /> | ||||
|   </c-card> | ||||
|   <!-- Decryption card UI --> | ||||
|   <c-card title="Decrypt"> | ||||
|     <div flex gap-3> | ||||
|       <c-input-text | ||||
| @ -57,12 +301,18 @@ const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.valu | ||||
|       /> | ||||
|       <div flex flex-1 flex-col gap-2> | ||||
|         <c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text /> | ||||
| 
 | ||||
|         <c-select | ||||
|           v-model:value="decryptAlgo" | ||||
|           label="Encryption algorithm:" | ||||
|           :options="Object.keys(algos).map((label) => ({ label, value: label }))" | ||||
|         /> | ||||
|         <!-- Show modes only if AES is selected --> | ||||
|         <c-select | ||||
|           v-if="decryptAlgo === 'AES'" | ||||
|           v-model:value="decryptAesMode" | ||||
|           label="AES mode:" | ||||
|           :options="aesModes" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|     <c-alert v-if="decryptError" type="error" mt-12 title="Error while decrypting"> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user