Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 258 additions & 8 deletions src/tools/encryption/encryption.vue
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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">
Expand Down
Loading