Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add crypto-js and nanoid , add add crypto demo page #4835

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions packages/@core/base/shared/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export default defineBuildConfig({
'src/color/index',
'src/cache/index',
'src/global-state',
'src/crypto/index',
],
});
12 changes: 12 additions & 0 deletions packages/@core/base/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
"types": "./dist/global-state.d.ts",
"development": "./src/global-state.ts",
"default": "./dist/global-state.mjs"
},
"./crypto": {
"types": "./dist/crypto/index.d.ts",
"development": "./src/crypto/index.ts",
"default": "./dist/crypto/index.mjs"
}
},
"publishConfig": {
Expand Down Expand Up @@ -75,6 +80,10 @@
"./global-state": {
"types": "./dist/global-state.d.ts",
"default": "./dist/global-state.mjs"
},
"./crypto": {
"types": "./dist/crypto/index.d.ts",
"default": "./dist/crypto/index.mjs"
}
}
},
Expand All @@ -84,14 +93,17 @@
"@types/lodash.get": "catalog:",
"@vue/shared": "catalog:",
"clsx": "catalog:",
"crypto-js": "catalog:",
"defu": "catalog:",
"lodash.clonedeep": "catalog:",
"lodash.get": "catalog:",
"nanoid": "catalog:",
"nprogress": "catalog:",
"tailwind-merge": "catalog:",
"theme-colors": "catalog:"
},
"devDependencies": {
"@types/crypto-js": "catalog:",
"@types/lodash.clonedeep": "catalog:",
"@types/nprogress": "catalog:"
}
Expand Down
33 changes: 33 additions & 0 deletions packages/@core/base/shared/src/crypto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import CryptoJS from 'crypto-js';

export { CryptoJS };
const MIN_SECRET_LENGTH = 32;
export class Crypto<T extends object> {
/** Secret */
private readonly secret: string;
constructor(secret: string) {
if (typeof secret === 'string' && secret.length < MIN_SECRET_LENGTH) {
throw new Error(
`Secret must be at least ${MIN_SECRET_LENGTH} characters long`,
);
}
this.secret = secret;
}
Comment on lines +8 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance secret key validation and handling.

The current implementation only validates the length of the secret. Consider these security improvements:

  1. Validate secret entropy/composition
  2. Use a key derivation function (KDF) to strengthen the secret
 constructor(secret: string) {
   if (typeof secret === 'string' && secret.length < MIN_SECRET_LENGTH) {
     throw new Error(
       `Secret must be at least ${MIN_SECRET_LENGTH} characters long`,
     );
   }
+  if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/.test(secret)) {
+    throw new Error('Secret must contain uppercase, lowercase, numbers, and special characters');
+  }
-  this.secret = secret;
+  // Derive a strong key using PBKDF2
+  this.secret = CryptoJS.PBKDF2(secret, 'salt', { keySize: 256/32 }).toString();
 }

Committable suggestion skipped: line range outside the PR's diff.


decrypt(encrypted: string) {
const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret);
const dataString = decrypted.toString(CryptoJS.enc.Utf8);
try {
return JSON.parse(dataString) as T;
} catch {
// avoid parse error
return null;
}
}
Comment on lines +17 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve decrypt method security and error handling.

The current implementation has several security considerations:

  1. Missing input validation
  2. Silent failure could hide security issues
  3. No IV/salt handling for AES
-decrypt(encrypted: string) {
+decrypt(encrypted: string): T | null {
+  if (!encrypted || typeof encrypted !== 'string') {
+    throw new Error('Invalid encrypted data');
+  }
+
   const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret);
+  if (!decrypted) {
+    throw new Error('Decryption failed');
+  }
+
   const dataString = decrypted.toString(CryptoJS.enc.Utf8);
   try {
     return JSON.parse(dataString) as T;
   } catch {
-    // avoid parse error
+    console.warn('Failed to parse decrypted data');
     return null;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
decrypt(encrypted: string) {
const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret);
const dataString = decrypted.toString(CryptoJS.enc.Utf8);
try {
return JSON.parse(dataString) as T;
} catch {
// avoid parse error
return null;
}
}
decrypt(encrypted: string): T | null {
if (!encrypted || typeof encrypted !== 'string') {
throw new Error('Invalid encrypted data');
}
const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret);
if (!decrypted) {
throw new Error('Decryption failed');
}
const dataString = decrypted.toString(CryptoJS.enc.Utf8);
try {
return JSON.parse(dataString) as T;
} catch {
console.warn('Failed to parse decrypted data');
return null;
}
}


encrypt(data: T): string {
const dataString = JSON.stringify(data);
const encrypted = CryptoJS.AES.encrypt(dataString, this.secret);
return encrypted.toString();
}
Comment on lines +28 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance encrypt method security.

The encryption implementation should be strengthened with:

  1. Input validation
  2. Proper IV handling
  3. Error handling
-encrypt(data: T): string {
+encrypt(data: T): string {
+  if (!data || typeof data !== 'object') {
+    throw new Error('Invalid data for encryption');
+  }
+
   const dataString = JSON.stringify(data);
-  const encrypted = CryptoJS.AES.encrypt(dataString, this.secret);
+  // Generate a random IV for each encryption
+  const iv = CryptoJS.lib.WordArray.random(16);
+  const encrypted = CryptoJS.AES.encrypt(dataString, this.secret, {
+    iv,
+    mode: CryptoJS.mode.CBC,
+    padding: CryptoJS.pad.Pkcs7
+  });
+  
+  // Include IV in the output for decryption
+  return JSON.stringify({
+    iv: iv.toString(),
+    content: encrypted.toString()
+  });
-  return encrypted.toString();
 }

Note: The decrypt method will need to be updated to handle the new encrypted data format that includes the IV.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
encrypt(data: T): string {
const dataString = JSON.stringify(data);
const encrypted = CryptoJS.AES.encrypt(dataString, this.secret);
return encrypted.toString();
}
encrypt(data: T): string {
if (!data || typeof data !== 'object') {
throw new Error('Invalid data for encryption');
}
const dataString = JSON.stringify(data);
// Generate a random IV for each encryption
const iv = CryptoJS.lib.WordArray.random(16);
const encrypted = CryptoJS.AES.encrypt(dataString, this.secret, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// Include IV in the output for decryption
return JSON.stringify({
iv: iv.toString(),
content: encrypted.toString()
});
}

}
21 changes: 21 additions & 0 deletions packages/@core/base/shared/src/utils/__tests__/nanoid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';

import { nanoid } from '../nanoid';

describe('nanoid', () => {
it('create uuid', () => {
const _nanoid = nanoid();
// console.log('uuid:', _nanoid);
// expect(!!_nanoid).toBe(true);
expect(typeof _nanoid).toBe('string');
expect(_nanoid.length).toBeGreaterThan(0);
});
it('should generate unique ids', () => {
const ids = new Set();
for (let i = 0; i < 1000; i++) {
const id = nanoid();
expect(ids.has(id)).toBe(false);
ids.add(id);
}
});
});
1 change: 1 addition & 0 deletions packages/@core/base/shared/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './dom';
export * from './inference';
export * from './letter';
export * from './merge';
export * from './nanoid';
export * from './nprogress';
export * from './state-handler';
export * from './to';
Expand Down
1 change: 1 addition & 0 deletions packages/@core/base/shared/src/utils/nanoid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { nanoid } from 'nanoid';
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './helpers';
export * from '@vben-core/shared/cache';
export * from '@vben-core/shared/color';
export * from '@vben-core/shared/crypto';
export * from '@vben-core/shared/utils';
9 changes: 9 additions & 0 deletions playground/src/router/routes/modules/demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,15 @@ const routes: RouteRecordRaw[] = [
title: 'Tanstack Query',
},
},
{
name: 'CryptoDemo',
path: '/demos/features/crypto',
component: () => import('#/views/demos/features/crypto/index.vue'),
meta: {
icon: 'lucide:message-square-lock',
title: 'Crypto Demo',
},
},
Comment on lines +224 to +232
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Fix title formatting and use translation key pattern

The route configuration needs to be updated to follow the established patterns in the codebase:

  1. Almost all routes in the file use $t() for translations, with features following the pattern demos.features.<name>
  2. The title has a double space: "Crypto Demo"
  3. Most feature demos have keepAlive: true configured
 {
   name: 'CryptoDemo',
   path: '/demos/features/crypto',
   component: () => import('#/views/demos/features/crypto/index.vue'),
   meta: {
     icon: 'lucide:message-square-lock',
-    title: 'Crypto  Demo',
+    title: $t('demos.features.crypto'),
+    keepAlive: true,
   },
 },
🔗 Analysis chain

Fix title formatting and consider adding translations and keepAlive.

While the route configuration is well-structured and appropriately placed, there are a few improvements to consider:

  1. The title contains a double space: "Crypto Demo"
  2. Consider using translation keys like other routes (e.g., $t('demos.features.crypto'))
  3. Consider adding keepAlive: true if the demo page contains forms or state that should be preserved
 {
   name: 'CryptoDemo',
   path: '/demos/features/crypto',
   component: () => import('#/views/demos/features/crypto/index.vue'),
   meta: {
     icon: 'lucide:message-square-lock',
-    title: 'Crypto  Demo',
+    title: $t('demos.features.crypto'),
+    keepAlive: true,
   },
 },

Let's verify if the translation key exists:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the translation key for crypto demo exists
rg -g '*.{ts,js,json}' "demos\.features\.crypto"

Length of output: 50


Script:

#!/bin/bash
# Let's check the localization files and patterns used in the codebase
# First, find translation files
fd -e json -e yaml locales

# Then check other route titles to understand the translation pattern
rg -g '*.{ts,js}' 'title: \$t\(' routes/

Length of output: 119


Script:

#!/bin/bash
# Let's check the route file itself to understand the pattern used for titles
rg -g '*.ts' 'title:' playground/src/router/routes/modules/demos.ts

# Also check for keepAlive usage in the same file
rg -g '*.ts' 'keepAlive:' playground/src/router/routes/modules/demos.ts

Length of output: 2981

],
},
// 面包屑导航
Expand Down
215 changes: 215 additions & 0 deletions playground/src/views/demos/features/crypto/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';

import {
Card,
Col,
Divider,
Input,
RadioButton,
RadioGroup,
Row,
Textarea,
} from 'ant-design-vue';

import {
aesDecryptFn,
aesEncryptFn,
desDecryptFn,
desEncryptFn,
hashingFn,
} from './inner/crypto-hooks';

const targetInputTextRef = ref('testtest');
const secretKeyRef = ref('3mbzyxbpg6613ql');
const encryModeRef = ref('aes');

const aseEncryptTextRef = ref('');
const needDecryptTextRef = ref('');
Comment on lines +23 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security: Remove hardcoded sensitive values.

The following security issues need to be addressed:

  1. Hardcoded test values should be removed from production code
  2. Secret keys should never be exposed in the source code

Apply this diff to remove hardcoded values:

-const targetInputTextRef = ref('testtest');
-const secretKeyRef = ref('3mbzyxbpg6613ql');
+const targetInputTextRef = ref('');
+const secretKeyRef = ref('');

Consider:

  • Using environment variables for any necessary default values
  • Adding input validation for minimum key strength
  • Adding a warning about secure key management

Committable suggestion skipped: line range outside the PR's diff.


const doAesEncryptFn = () => {
if (!targetInputTextRef.value?.trim() || !secretKeyRef.value?.trim()) {
return;
}
try {
const text =
encryModeRef.value === 'aes'
? aesEncryptFn(targetInputTextRef.value, secretKeyRef.value)
: desEncryptFn(targetInputTextRef.value, secretKeyRef.value);
aseEncryptTextRef.value = text;
needDecryptTextRef.value = text;
} catch (error) {
console.error('Encryption failed:', error);
// Consider using a notification system to show errors
}
};
Comment on lines +30 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and function naming.

The encryption function has several areas for improvement:

  1. Function name doAesEncryptFn is misleading as it handles both AES and DES
  2. Error handling only logs to console without user feedback
  3. Missing validation for key strength and format

Consider this improved implementation:

-const doAesEncryptFn = () => {
+const performEncryption = () => {
   if (!targetInputTextRef.value?.trim() || !secretKeyRef.value?.trim()) {
+    // Use your app's notification system (e.g., message.error)
+    showError('Please provide both input text and secret key');
     return;
   }
+  
+  // Validate key strength
+  if (secretKeyRef.value.length < 16) {
+    showError('Secret key must be at least 16 characters long');
+    return;
+  }

   try {
     const text =
       encryModeRef.value === 'aes'
         ? aesEncryptFn(targetInputTextRef.value, secretKeyRef.value)
         : desEncryptFn(targetInputTextRef.value, secretKeyRef.value);
     aseEncryptTextRef.value = text;
     needDecryptTextRef.value = text;
   } catch (error) {
-    console.error('Encryption failed:', error);
-    // Consider using a notification system to show errors
+    showError(`Encryption failed: ${error.message}`);
   }
 };

Committable suggestion skipped: line range outside the PR's diff.


const parseTextComputed = computed(() => {
const value =
encryModeRef.value === 'aes'
? aesDecryptFn(needDecryptTextRef.value, secretKeyRef.value)
: desDecryptFn(needDecryptTextRef.value, secretKeyRef.value);
return value;
});
watch(
[
() => targetInputTextRef.value,
() => secretKeyRef.value,
() => encryModeRef.value,
],
() => {
doAesEncryptFn();
},
{
immediate: true,
},
);
</script>

<template>
<div class="box-border w-full px-2">
<Card class="mb-3" title="使用Crypto加密">
<div>要操作的字符串</div>
<Input
v-model:value="targetInputTextRef"
placeholder="请输入要操作的字符串"
/>
<div>使用的密钥</div>
<AInput v-model:value="secretKeyRef" />
</Card>
<Row>
<Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24">
<Card class="mr-1" title="Hashing">
<div
class="px-10px box-border flex w-full flex-col divide-y overflow-x-hidden"
>
<div
class="flex h-auto w-full flex-row justify-between overflow-hidden"
>
<div class="flex-none">
<span :style="{ paddingRight: '10px' }">md5:</span>
</div>
<div class="h-auto flex-1 overflow-x-hidden">
<div class="w-full text-wrap break-words text-right">
{{ hashingFn(targetInputTextRef, 'MD5') }}
</div>
</div>
</div>
<div
class="flex h-auto w-full flex-row justify-between overflow-hidden"
>
<div class="flex-none">
<span :style="{ paddingRight: '10px' }">SHA1:</span>
</div>
<div class="h-auto flex-1 overflow-x-hidden">
<div class="w-full text-wrap break-words text-right">
{{ hashingFn(targetInputTextRef, 'SHA1') }}
</div>
</div>
</div>
<div
class="flex h-auto w-full flex-row justify-between overflow-hidden"
>
<div class="flex-none">
<span :style="{ paddingRight: '10px' }">SHA256:</span>
</div>
<div class="h-auto flex-1 overflow-x-hidden">
<div class="w-full text-wrap break-words text-right">
{{ hashingFn(targetInputTextRef, 'SHA256') }}
</div>
</div>
</div>
<div
class="flex h-auto w-full flex-row justify-between overflow-hidden"
>
<div class="flex-none">
<span :style="{ paddingRight: '10px' }">SHA512:</span>
</div>
<div class="h-auto flex-1 overflow-x-hidden">
<div class="w-full text-wrap break-words text-right">
{{ hashingFn(targetInputTextRef, 'SHA512') }}
</div>
</div>
</div>
</div>
</Card>
</Col>
<Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24">
<Card title="Ciphers">
<div
class="px-10px box-border flex w-full flex-col overflow-x-hidden"
>
<div
class="box-border flex flex-row items-center justify-between pb-2"
>
<div class="flex-none">加密方式:</div>
<div class="flex-1 text-center">
<RadioGroup
v-model:value="encryModeRef"
button-style="solid"
size="small"
>
<RadioButton value="aes">AES</RadioButton>
<RadioButton value="des">DES</RadioButton>
</RadioGroup>
</div>
</div>
<Divider />
<div v-if="encryModeRef === 'aes'">
<div
class="flex h-auto w-full flex-row justify-between overflow-hidden"
>
<div class="flex-none">
<span :style="{ paddingRight: '10px' }">加密:</span>
</div>
<div class="h-auto flex-1 overflow-x-hidden">
<div class="w-full text-wrap break-words text-right">
{{ aseEncryptTextRef }}
</div>
</div>
</div>
<Divider />
<div>
<div>解密:</div>
<div>要解密的文本</div>
<Textarea
v-model:value="needDecryptTextRef"
placeholder="请输入要操作的字符串"
/>
<div>解密后的原文</div>
<Textarea :value="parseTextComputed" readonly />
</div>
</div>
<div v-else-if="encryModeRef === 'des'">
<div
class="flex h-auto w-full flex-row justify-between overflow-hidden"
>
<div class="flex-none">
<span :style="{ paddingRight: '10px' }">加密:</span>
</div>
<div class="h-auto flex-1 overflow-x-hidden">
<div class="w-full text-wrap break-words text-right">
{{ aseEncryptTextRef }}
</div>
</div>
</div>
<Divider />
<div>
<div>解密:</div>
<div>要解密的文本</div>
<ATextarea
v-model:value="needDecryptTextRef"
placeholder="请输入要操作的字符串"
/>
<div>解密后的原文</div>
<Textarea :value="parseTextComputed" readonly />
</div>
</div>
</div>
</Card>
</Col>
</Row>
</div>
</template>

<style lang="scss" scoped></style>
Loading
Loading