├── public ├── logo │ ├── logo.png │ ├── logo-dark.png │ └── logo-light.png └── other │ ├── noise.webp │ ├── file_icon.ico │ └── file_icon.png ├── assets ├── images │ ├── app-icons │ │ ├── mac │ │ │ └── icon.icns │ │ ├── png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 64x64.png │ │ │ ├── 128x128.png │ │ │ ├── 256x256.png │ │ │ ├── 512x512.png │ │ │ └── 1024x1024.png │ │ └── win │ │ │ └── icon.ico │ └── setup-icons │ │ ├── header.bmp │ │ ├── sidebar.bmp │ │ ├── mac │ │ └── icon.icns │ │ ├── png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 64x64.png │ │ ├── 128x128.png │ │ ├── 256x256.png │ │ ├── 512x512.png │ │ └── 1024x1024.png │ │ └── win │ │ └── icon.ico ├── fonts │ ├── IBM │ │ ├── IBMPlexSerif-Bold.ttf │ │ ├── IBMPlexSerif-Medium.ttf │ │ ├── IBMPlexSerif-Regular.ttf │ │ └── IBMPlexSerif-SemiBold.ttf │ └── GeistMono │ │ └── GeistMono-VariableFont_wght.ttf └── scripts │ ├── installer.nsh │ └── config.nsi ├── postcss.config.cjs ├── src ├── vite-env.d.ts ├── Utils │ ├── copyText.tsx │ ├── getUserKeys.tsx │ ├── openLink.tsx │ ├── getAppVersion.tsx │ ├── AlgorithmUtil.tsx │ └── getDeviceOS.tsx ├── Components │ ├── ui │ │ ├── SwitchToggler.tsx │ │ └── Checkbox.tsx │ ├── AboutWindow.tsx │ ├── SettingsDropDown.tsx │ ├── ModalAlertBox.tsx │ ├── BottomInfo.tsx │ ├── InlineMessageBox.tsx │ ├── ToastNotification.tsx │ ├── Titlebar.tsx │ ├── UpdateModal.tsx │ ├── ViewUnpackedKeys.tsx │ └── MainDropDown.tsx ├── i18n.ts ├── globalSlice.ts ├── store │ ├── decryptionSlice.ts │ ├── encryptionSlice.ts │ └── encryptionMethodSlice.ts ├── Routes │ ├── components │ │ ├── ThemeCustomBox.tsx │ │ └── SettingsNav.tsx │ ├── tabs │ │ └── Appearance.tsx │ └── TextDecryption.tsx ├── store.ts ├── Context │ ├── ExtractContext.tsx │ ├── EmbedContext.tsx │ ├── KeysContext.tsx │ ├── ToastContext.tsx │ ├── DecryptContext.tsx │ └── EncryptContext.tsx ├── global.d.ts ├── Providers │ ├── StartupProvider.tsx │ ├── ThemeProvider.tsx │ └── RippleProvider.tsx ├── electron.d.ts ├── main.tsx └── Locales │ ├── en.json │ └── ru.json ├── .npmrc ├── vitest.config.ts ├── tsconfig.node.json ├── tailwind.config.js ├── index.html ├── .vscode └── settings.json ├── .gitignore ├── electron ├── main │ └── utils │ │ ├── moveExtractedFiles.ts │ │ ├── writeLog.ts │ │ ├── sanitizeFilePath.ts │ │ ├── trySaveHistory.ts │ │ ├── KeyService.ts │ │ ├── crypto.ts │ │ ├── validateFileDecryption.ts │ │ ├── encryptText.ts │ │ ├── decryptText.ts │ │ ├── decryptFile.ts │ │ ├── encryptFile.ts │ │ ├── extractHiddenData.ts │ │ └── hideDataInImage.ts ├── electron-env.d.ts └── preload │ └── index.ts ├── .github └── FUNDING.yml ├── tsconfig.json ├── .playwright.config.txt ├── package.json ├── .vite.config.flat.txt ├── vite.config.ts ├── electron-builder.json ├── README.md └── README.ru.md /public/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/public/logo/logo.png -------------------------------------------------------------------------------- /public/logo/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/public/logo/logo-dark.png -------------------------------------------------------------------------------- /public/other/noise.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/public/other/noise.webp -------------------------------------------------------------------------------- /public/logo/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/public/logo/logo-light.png -------------------------------------------------------------------------------- /public/other/file_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/public/other/file_icon.ico -------------------------------------------------------------------------------- /public/other/file_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/public/other/file_icon.png -------------------------------------------------------------------------------- /assets/images/app-icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/mac/icon.icns -------------------------------------------------------------------------------- /assets/images/app-icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/16x16.png -------------------------------------------------------------------------------- /assets/images/app-icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/24x24.png -------------------------------------------------------------------------------- /assets/images/app-icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/32x32.png -------------------------------------------------------------------------------- /assets/images/app-icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/48x48.png -------------------------------------------------------------------------------- /assets/images/app-icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/64x64.png -------------------------------------------------------------------------------- /assets/images/app-icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/win/icon.ico -------------------------------------------------------------------------------- /assets/images/setup-icons/header.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/header.bmp -------------------------------------------------------------------------------- /assets/images/setup-icons/sidebar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/sidebar.bmp -------------------------------------------------------------------------------- /assets/fonts/IBM/IBMPlexSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/fonts/IBM/IBMPlexSerif-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/IBM/IBMPlexSerif-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/fonts/IBM/IBMPlexSerif-Medium.ttf -------------------------------------------------------------------------------- /assets/images/app-icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/128x128.png -------------------------------------------------------------------------------- /assets/images/app-icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/256x256.png -------------------------------------------------------------------------------- /assets/images/app-icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/512x512.png -------------------------------------------------------------------------------- /assets/images/setup-icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/mac/icon.icns -------------------------------------------------------------------------------- /assets/images/setup-icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/16x16.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/24x24.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/32x32.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/48x48.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/64x64.png -------------------------------------------------------------------------------- /assets/images/setup-icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/win/icon.ico -------------------------------------------------------------------------------- /assets/fonts/IBM/IBMPlexSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/fonts/IBM/IBMPlexSerif-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/IBM/IBMPlexSerif-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/fonts/IBM/IBMPlexSerif-SemiBold.ttf -------------------------------------------------------------------------------- /assets/images/app-icons/png/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/app-icons/png/1024x1024.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/128x128.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/256x256.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/512x512.png -------------------------------------------------------------------------------- /assets/images/setup-icons/png/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/images/setup-icons/png/1024x1024.png -------------------------------------------------------------------------------- /assets/fonts/GeistMono/GeistMono-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AroCrypt/app/HEAD/assets/fonts/GeistMono/GeistMono-VariableFont_wght.ttf -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'tailwindcss/nesting': {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface Window { 4 | // expose in the `electron/preload/index.ts` 5 | ipcRenderer: import('electron').IpcRenderer 6 | } 7 | -------------------------------------------------------------------------------- /assets/scripts/installer.nsh: -------------------------------------------------------------------------------- 1 | !insertmacro MUI_PAGE_WELCOME 2 | !define MUI_WELCOMEPAGE_TITLE "Welcome to AroCrypt Setup" 3 | !define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through installing AroCrypt." 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # For electron-builder 2 | # https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 3 | shamefully-hoist=true 4 | 5 | # For China 🇨🇳 developers 6 | # electron_mirror=https://npmmirror.com/mirrors/electron/ 7 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | root: __dirname, 6 | include: ['test/**/*.{test,spec}.?(c|m)[jt]s?(x)'], 7 | testTimeout: 1000 * 29, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /src/Utils/copyText.tsx: -------------------------------------------------------------------------------- 1 | export default async function CopyText(text: any) { 2 | try { 3 | await navigator.clipboard.writeText(text); 4 | return "success"; 5 | } catch (err) { 6 | console.error('Failed to copy text: ', err); 7 | return "error"; 8 | } 9 | } -------------------------------------------------------------------------------- /src/Utils/getUserKeys.tsx: -------------------------------------------------------------------------------- 1 | export default async function getKeys() { 2 | try { 3 | const getKey = await window.electronAPI.getKeys(); 4 | return getKey; 5 | } catch (error) { 6 | console.error('Failed to get user keys:', error); 7 | } 8 | } -------------------------------------------------------------------------------- /src/Utils/openLink.tsx: -------------------------------------------------------------------------------- 1 | const useOpenLink = async (link: any) => { 2 | try { 3 | await window.electronAPI.openExternalLink(link); 4 | } catch (error) { 5 | console.error('Failed to open external link:', error); 6 | } 7 | 8 | } 9 | 10 | export default useOpenLink -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts", "package.json"] 10 | } 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './index.html', 5 | './src/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | corePlugins: { 11 | preflight: false, 12 | }, 13 | plugins: [], 14 | } 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | AroCrypt 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "arocrypt", 4 | "chacha", 5 | "Ciphertext", 6 | "datahider", 7 | "dfile", 8 | "dtext", 9 | "efile", 10 | "encap", 11 | "etext", 12 | "extracter", 13 | "maximizable", 14 | "minimizable", 15 | "mlkem", 16 | "Screenable", 17 | "steg", 18 | "unmaximize", 19 | "Расшифровать", 20 | "Шифрование", 21 | "шифрования" 22 | ] 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dist-electron 14 | releases 15 | certs 16 | temp-electron-builder.json 17 | *.local 18 | 19 | # Editor directories and files 20 | .vscode/.debug.env 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | #lockfile 30 | package-lock.json 31 | pnpm-lock.yaml 32 | yarn.lock 33 | /test-results/ 34 | /playwright-report/ 35 | /playwright/.cache/ 36 | 37 | #other 38 | .env 39 | .npmrc -------------------------------------------------------------------------------- /src/Components/ui/SwitchToggler.tsx: -------------------------------------------------------------------------------- 1 | interface InterfaceSwitchToggler { 2 | isOn: boolean, 3 | onToggle: Function, 4 | isDisabled?: boolean, 5 | } 6 | 7 | const SwitchToggler = ({ isOn, onToggle, isDisabled }: InterfaceSwitchToggler) => { 8 | const Toggle = () => onToggle(!isOn) 9 | 10 | return ( 11 | 18 | ) 19 | } 20 | 21 | export default SwitchToggler -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import enTranslations from './Locales/en.json'; 5 | import ruTranslations from './Locales/ru.json'; 6 | 7 | i18n 8 | .use(initReactI18next) 9 | .init({ 10 | resources: { 11 | en: { translation: enTranslations }, 12 | ru: { translation: ruTranslations } 13 | }, 14 | lng: localStorage.getItem('language') || 'en', 15 | fallbackLng: 'en', 16 | interpolation: { 17 | escapeValue: false 18 | } 19 | }); 20 | 21 | export default i18n; -------------------------------------------------------------------------------- /electron/main/utils/moveExtractedFiles.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | const moveExtractedFiles = async ( 5 | extractedPaths: string[], 6 | finalOutputPath: string 7 | ): Promise => { 8 | const movedPaths: string[] = []; 9 | 10 | for (const filePath of extractedPaths) { 11 | const fileName = path.basename(filePath); 12 | const destinationPath = path.join(finalOutputPath, fileName); 13 | 14 | // Move the file 15 | await fs.promises.rename(filePath, destinationPath); 16 | movedPaths.push(destinationPath); 17 | } 18 | 19 | return movedPaths; 20 | }; 21 | 22 | export default moveExtractedFiles; -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: 'true' 6 | APP_ROOT?: string 7 | VITE_DEV_SERVER_URL?: string 8 | VITE_PUBLIC?: string 9 | /** 10 | * The built directory structure 11 | * 12 | * ```tree 13 | * ├─┬ dist-electron 14 | * │ ├─┬ main 15 | * │ │ └── index.js 16 | * │ └─┬ preload 17 | * │ └── index.js 18 | * ├─┬ dist 19 | * │ └── index.html 20 | * ``` 21 | */ 22 | DIST_ELECTRON: string 23 | DIST: string 24 | /** /dist/ or /public/ */ 25 | PUBLIC: string 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Components/ui/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | interface InterfaceCheckbox { 2 | isChecked: boolean, 3 | onToggle: Function, 4 | } 5 | 6 | const Checkbox = ({ isChecked, onToggle }: InterfaceCheckbox) => { 7 | const Toggle = () => onToggle(!isChecked); 8 | 9 | return ( 10 |
14 | 15 |
16 | ) 17 | } 18 | 19 | export default Checkbox -------------------------------------------------------------------------------- /src/globalSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import i18n from './i18n'; 3 | 4 | const initialState = { 5 | language: localStorage.getItem('language') || 'en', 6 | }; 7 | 8 | const globalSlice = createSlice({ 9 | name: 'global', 10 | initialState, 11 | reducers: { 12 | setLanguage: (state, action) => { 13 | const selectedLanguage = action.payload; 14 | state.language = selectedLanguage; 15 | localStorage.setItem('language', selectedLanguage); 16 | i18n.changeLanguage(selectedLanguage); 17 | } 18 | }, 19 | }); 20 | 21 | export const { setLanguage } = globalSlice.actions; 22 | export default globalSlice.reducer; 23 | -------------------------------------------------------------------------------- /src/Utils/getAppVersion.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export const useAppVersion = () => { 4 | const [appVersion, setAppVersion] = useState('') 5 | 6 | useEffect(() => { 7 | const fetchAppVersion = async () => { 8 | try { 9 | const version = await window.electronAPI.getAppVersion(); 10 | setAppVersion(version); 11 | } catch (error) { 12 | console.error('Failed to fetch app version:', error); 13 | setAppVersion('[unknown]'); 14 | } 15 | }; 16 | 17 | fetchAppVersion(); 18 | }, []); 19 | 20 | return appVersion; 21 | } 22 | 23 | export default useAppVersion; -------------------------------------------------------------------------------- /electron/main/utils/writeLog.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from 'fs'; 3 | 4 | export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; 5 | const isProduction = 6 | process.env.NODE_ENV === "production" || !VITE_DEV_SERVER_URL; 7 | 8 | export function safeWriteLog(message: any) { 9 | if (isProduction) return; 10 | 11 | const logDir = path.join(process.cwd(), 'logs'); 12 | fs.mkdirSync(logDir, { recursive: true }); 13 | 14 | const timestamp = new Date().toISOString(); 15 | const logFilePath = path.join(logDir, 'app-log.txt'); 16 | 17 | try { 18 | fs.appendFileSync(logFilePath, `${timestamp} - ${message}\n`); 19 | console.log(message); 20 | } catch (error) { 21 | console.error('Logging failed:', error); 22 | } 23 | } -------------------------------------------------------------------------------- /src/store/decryptionSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | interface DecryptionState { 4 | packedKeys: string; 5 | decrypted_text: string; 6 | } 7 | 8 | const initialState: DecryptionState = { 9 | packedKeys: '', 10 | decrypted_text: '' 11 | }; 12 | 13 | const decryptionSlice = createSlice({ 14 | name: 'decryption', 15 | initialState, 16 | reducers: { 17 | setPackedKeys: (state, action: PayloadAction) => { 18 | state.packedKeys = action.payload; 19 | }, 20 | setDecryptedText: (state, action: PayloadAction) => { 21 | state.decrypted_text = action.payload; 22 | } 23 | } 24 | }); 25 | 26 | export const { setPackedKeys, setDecryptedText } = decryptionSlice.actions; 27 | export default decryptionSlice.reducer; -------------------------------------------------------------------------------- /src/store/encryptionSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | interface EncryptionState { 4 | encryptText: string; 5 | output_PackedKeys: string; 6 | } 7 | 8 | const initialState: EncryptionState = { 9 | encryptText: '', 10 | output_PackedKeys: '', 11 | }; 12 | 13 | const encryptionSlice = createSlice({ 14 | name: 'encryption', 15 | initialState, 16 | reducers: { 17 | setOutputPackedKeys: (state, action: PayloadAction) => { 18 | state.output_PackedKeys = action.payload; 19 | }, 20 | setEncryptText: (state, action: PayloadAction) => { 21 | state.encryptText = action.payload; 22 | } 23 | } 24 | }); 25 | 26 | export const { setOutputPackedKeys, setEncryptText } = encryptionSlice.actions; 27 | export default encryptionSlice.reducer; -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: AroCrypt 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: arocodes 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /src/Components/AboutWindow.tsx: -------------------------------------------------------------------------------- 1 | import useAppVersion from "@/Utils/getAppVersion"; 2 | import useOpenLink from "@/Utils/openLink"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | export default function AboutWindow(): JSX.Element { 6 | const { t } = useTranslation(); 7 | const appVersion = useAppVersion(); 8 | 9 | return ( 10 |
11 | Logo 12 |
13 |

AroCrypt useOpenLink(`https://github.com/AroCrypt/app/releases`)}>v{appVersion}

14 |

{t("about_page_info")}

15 |

{t('about_dev_info')} useOpenLink("https://github.com/OfficialAroCodes")}>AroCodes.

16 |
17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /src/Routes/components/ThemeCustomBox.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "@/Providers/ThemeProvider"; 2 | 3 | const ThemeCustomBox = ({ mode }: { mode: "dark" | "light" }) => { 4 | const { themeType, setThemeType } = useTheme(); 5 | 6 | return ( 7 |
setThemeType(mode)} 10 | > 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 |
21 | ) 22 | } 23 | 24 | export default ThemeCustomBox 25 | -------------------------------------------------------------------------------- /src/Utils/AlgorithmUtil.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Available_Algorithms = [ 4 | 'AES-256-GCM', 5 | 'AES-192-GCM', 6 | 'AES-128-GCM', 7 | 'AES-256-CBC', 8 | 'AES-192-CBC', 9 | 'AES-128-CBC', 10 | 'AES-256-CTR', 11 | 'AES-192-CTR', 12 | 'AES-128-CTR', 13 | ]; 14 | 15 | const DEFAULT_ALGO = 'AES-256-GCM'; 16 | 17 | const getAlgorithm = (key: string) => { 18 | const raw = localStorage.getItem(key); 19 | if (raw && Available_Algorithms.includes(raw)) return raw; 20 | localStorage.setItem(key, DEFAULT_ALGO); 21 | return DEFAULT_ALGO; 22 | }; 23 | 24 | export const CheckAlgorithm = () => { 25 | getAlgorithm('encryptionMethod'); 26 | getAlgorithm('decryptionMethod'); 27 | }; 28 | 29 | const Algorithm: React.FC = () => { 30 | CheckAlgorithm(); 31 | return null; 32 | }; 33 | 34 | export default Algorithm; 35 | -------------------------------------------------------------------------------- /electron/main/utils/sanitizeFilePath.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from 'fs'; 3 | import { safeWriteLog } from "./writeLog"; 4 | 5 | export function sanitizeFilePath(inputPath: string, allowFileCreation: boolean = false): string { 6 | try { 7 | const normalizedPath = path.normalize(inputPath); 8 | const absolutePath = path.resolve(normalizedPath); 9 | 10 | if (!allowFileCreation && !fs.existsSync(absolutePath)) { 11 | throw new Error(`File does not exist: ${absolutePath}`); 12 | } 13 | 14 | if (allowFileCreation) { 15 | const directory = path.dirname(absolutePath); 16 | fs.mkdirSync(directory, { recursive: true }); 17 | } 18 | 19 | return absolutePath; 20 | } catch (error) { 21 | safeWriteLog(`Path sanitization error: ${error}`); 22 | throw new Error(`Invalid file path: ${inputPath}`); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Utils/getDeviceOS.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function getDeviceOS() { 4 | const [osType, setOsType] = useState<'mac' | 'win' | 'linux' | null>(null); 5 | let platform = ''; 6 | 7 | if (window?.electronAPI?.getPlatform) { 8 | window.electronAPI.getPlatform().then((result: string) => { 9 | platform = result; 10 | if (platform.startsWith('win')) setOsType('win'); 11 | else if (platform.startsWith('darwin') || platform === 'mac') setOsType('mac'); 12 | else if (platform.startsWith('linux')) setOsType('linux'); 13 | else setOsType(null); 14 | }).catch(() => setOsType(null)); 15 | } else if (navigator?.userAgent) { 16 | const ua = navigator.userAgent.toLowerCase(); 17 | if (ua.indexOf('win') !== -1) setOsType('win'); 18 | else if (ua.indexOf('mac') !== -1) setOsType('mac'); 19 | else if (ua.indexOf('linux') !== -1) setOsType('linux'); 20 | else setOsType(null); 21 | } 22 | 23 | return osType; 24 | } -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import globalReducer from './globalSlice'; 3 | import encryptionReducer from './store/encryptionSlice'; 4 | import decryptionSlice from './store/decryptionSlice'; 5 | import encryptionMethodReducer from './store/encryptionMethodSlice'; 6 | import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; 7 | 8 | export const store = configureStore({ 9 | reducer: { 10 | global: globalReducer, 11 | encryption: encryptionReducer, 12 | decryption: decryptionSlice, 13 | encryptionMethod: encryptionMethodReducer, 14 | }, 15 | middleware: (getDefaultMiddleware) => 16 | getDefaultMiddleware({ 17 | serializableCheck: false, 18 | }), 19 | }); 20 | 21 | export type RootState = ReturnType; 22 | export type AppDispatch = typeof store.dispatch; 23 | 24 | export const useAppDispatch: () => AppDispatch = useDispatch; 25 | export const useAppSelector: TypedUseSelectorHook = useSelector; 26 | 27 | export default store; -------------------------------------------------------------------------------- /src/Context/ExtractContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | export type File = { 5 | path: string; 6 | name: string; 7 | }; 8 | 9 | type ExtractContextType = { 10 | files: File[]; 11 | setFiles: (files: File[]) => void; 12 | }; 13 | 14 | const ExtractContext = createContext(undefined); 15 | 16 | export const useExtract = () => { 17 | const ctx = useContext(ExtractContext); 18 | if (!ctx) throw new Error('useExtract must be used within DecryptProvider'); 19 | return ctx; 20 | }; 21 | 22 | export const ExtractProvider = ({ children }: { children: ReactNode }) => { 23 | const [files, setFiles] = useState([]); 24 | const navigate = useNavigate(); 25 | 26 | useEffect(() => { 27 | if (files.length > 0) { 28 | navigate('/steganography/extract'); 29 | } 30 | }, [files]); 31 | 32 | return ( 33 | 34 | {children} 35 | 36 | ); 37 | }; -------------------------------------------------------------------------------- /src/Context/EmbedContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | export type File = { 5 | path: string; 6 | name: string; 7 | }; 8 | 9 | type EmbedContextType = { 10 | files: File[]; 11 | setFiles: (files: File[]) => void; 12 | secretFiles: File[]; 13 | setSecretFiles: (files: File[]) => void; 14 | }; 15 | 16 | const EmbedContext = createContext(undefined); 17 | 18 | export const useEmbed = () => { 19 | const ctx = useContext(EmbedContext); 20 | if (!ctx) throw new Error('useEmbed must be used within DecryptProvider'); 21 | return ctx; 22 | }; 23 | 24 | export const EmbedProvider = ({ children }: { children: ReactNode }) => { 25 | const [files, setFiles] = useState([]); 26 | const [secretFiles, setSecretFiles] = useState([]); 27 | const navigate = useNavigate(); 28 | 29 | useEffect(() => { 30 | if (files.length > 0) { 31 | navigate('/steganography/hide'); 32 | } 33 | }, [files]); 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }; -------------------------------------------------------------------------------- /src/store/encryptionMethodSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | const DEFAULT_ENCRYPTION_METHOD = 'AES-256-GCM'; 4 | 5 | interface EncryptionMethodState { 6 | encryptionMethod: string; 7 | decryptionMethod: string; 8 | } 9 | 10 | const loadMethodFromStorage = (key: string) => 11 | localStorage.getItem(key) || DEFAULT_ENCRYPTION_METHOD; 12 | 13 | const initialState: EncryptionMethodState = { 14 | encryptionMethod: loadMethodFromStorage('encryptionMethod'), 15 | decryptionMethod: loadMethodFromStorage('decryptionMethod') 16 | }; 17 | 18 | const encryptionMethodSlice = createSlice({ 19 | name: 'encryptionMethod', 20 | initialState, 21 | reducers: { 22 | setEncryptionMethod: (state, action: PayloadAction) => { 23 | state.encryptionMethod = action.payload; 24 | localStorage.setItem('encryptionMethod', action.payload); 25 | }, 26 | setDecryptionMethod: (state, action: PayloadAction) => { 27 | state.decryptionMethod = action.payload; 28 | localStorage.setItem('decryptionMethod', action.payload); 29 | } 30 | } 31 | }); 32 | 33 | export const { 34 | setEncryptionMethod, 35 | setDecryptionMethod 36 | } = encryptionMethodSlice.actions; 37 | 38 | export default encryptionMethodSlice.reducer; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": [ 4 | "./node_modules/@types" 5 | ], 6 | "types": ["node"], 7 | "target": "ES2020", 8 | "useDefineForClassFields": true, 9 | "lib": [ 10 | "ES2020", 11 | "DOM", 12 | "DOM.Iterable" 13 | ], 14 | "allowJs": false, 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "strict": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "module": "ESNext", 21 | "moduleResolution": "node", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "noEmit": true, 25 | "jsx": "react-jsx", 26 | "baseUrl": ".", 27 | "paths": { 28 | "@/*": ["src/*"], 29 | "@Components/*": ["src/Components/*"], 30 | "@Routes/*": ["src/Routes/*"], 31 | "@Locales/*": ["src/Locales/*"], 32 | "@Providers/*": ["src/Providers/*"], 33 | "@Utils/*": ["src/Utils/*"] 34 | } 35 | }, 36 | "include": [ 37 | "src", 38 | "src/electron.d.ts", 39 | "electron", 40 | "src/**/*.ts", 41 | "src/**/*.tsx", 42 | "electron/**/*.ts", 43 | "electron/**/*.tsx", 44 | "src/global.d.ts" 45 | ], 46 | "exclude": [ 47 | "node_modules" 48 | ], 49 | "references": [ 50 | { 51 | "path": "./tsconfig.node.json" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /src/Context/KeysContext.tsx: -------------------------------------------------------------------------------- 1 | import KeysProvider from '@/Providers/KeysProvider'; 2 | import React, { createContext, useContext, useState, useCallback } from 'react'; 3 | import { useToast } from './ToastContext'; 4 | 5 | interface KeyProviderContextType { 6 | openManual: () => void; 7 | openAuto: () => void; 8 | close: () => void; 9 | } 10 | 11 | const KeyProviderContext = createContext(null); 12 | 13 | export const useKeyProvider = () => { 14 | const ctx = useContext(KeyProviderContext); 15 | if (!ctx) throw new Error("useKeyProvider must be used within KeyProviderProvider"); 16 | return ctx; 17 | }; 18 | 19 | export const KeysMainProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 20 | const [manualOpen, setManualOpen] = useState(false); 21 | const [autoOpen, setAutoOpen] = useState(false); 22 | 23 | const openManual = useCallback(() => setManualOpen(true), []); 24 | const openAuto = useCallback(() => setAutoOpen(true), []); 25 | 26 | const close = useCallback(() => { 27 | setManualOpen(false); 28 | setAutoOpen(false); 29 | }, []); 30 | 31 | return ( 32 | 33 | {children} 34 | { 35 | close(); 36 | }} /> 37 | 38 | ); 39 | }; -------------------------------------------------------------------------------- /electron/main/utils/trySaveHistory.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import { saveHistory } from "./db/DBService"; 3 | import { safeWriteLog } from "./writeLog"; 4 | 5 | export async function trySaveHistory( 6 | operation: "dtext_logs" | "dfile_logs" | "etext_logs" | "efile_logs" | "steg-in_logs" | "steg-out_logs", 7 | status: "success" | "fail" | "canceled", 8 | inputPath: string, 9 | outputPath: string, 10 | algorithm: string, 11 | startTime: number 12 | ) { 13 | const duration = Date.now() - startTime; 14 | 15 | let columns: string[] = ["algorithm", "status", "duration"]; 16 | let values: any[] = [algorithm, status, duration]; 17 | let outputStats; 18 | let inputStats; 19 | 20 | try { 21 | inputStats = await fs.stat(inputPath); 22 | } catch (err) { 23 | inputStats = { input_path: "N\A", outputPath: "N\A", output_size: 0 }; 24 | } 25 | 26 | try { 27 | outputStats = await fs.stat(outputPath); 28 | } catch (err) { 29 | outputStats = { input_path: "N\A", outputPath: "N\A", output_size: 0 }; 30 | } 31 | 32 | if (["efile_logs", "dfile_logs", "steg-in_logs", "steg-out_logs"].includes(operation)) { 33 | columns = ["input_path", "output_path", "input_size", "output_size", "algorithm", "status", "duration"]; 34 | values = [inputPath, outputPath, inputStats.size, outputStats.size, algorithm, status, duration]; 35 | } 36 | 37 | try { 38 | await saveHistory(operation, columns, values); 39 | } catch (err) { 40 | safeWriteLog(`Failed to save history [${status}]: ${err}`); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Context/ToastContext.tsx: -------------------------------------------------------------------------------- 1 | import ToastNotification from "@/Components/ToastNotification"; 2 | import { createContext, useContext, useState, ReactNode } from "react"; 3 | 4 | type Toast = { 5 | id: number; 6 | type: number; 7 | message: string; 8 | title: string; 9 | }; 10 | 11 | const ToastContext = createContext<(type: number, message: string, title: string) => void>(() => { }); 12 | 13 | let idCounter = 0; 14 | 15 | export const ToastProvider = ({ children }: { children: ReactNode }) => { 16 | const [toasts, setToasts] = useState([]); 17 | 18 | const showToast = (type: number, message: string, title: string) => { 19 | const id = idCounter++; 20 | setToasts((prev) => [...prev, { id, type, message, title }]); 21 | }; 22 | 23 | return ( 24 | 25 | {children} 26 |
27 | {toasts.map(({ id, type, message, title }) => ( 28 | { 34 | setToasts((prev) => prev.filter((t) => t.id !== id)); 35 | }} 36 | /> 37 | ))} 38 |
39 |
40 | ); 41 | }; 42 | 43 | export const useToast = () => useContext(ToastContext); 44 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'electron-store' { 2 | interface ElectronStore { 3 | get(key: K): T[K]; 4 | } 5 | } 6 | 7 | declare module 'Components' { 8 | const components: { 9 | [key: string]: React.ComponentType; 10 | }; 11 | export = components; 12 | } 13 | 14 | declare module 'Locales' { 15 | const locales: { 16 | [key: string]: any; 17 | }; 18 | export = locales; 19 | } 20 | 21 | declare module 'Providers' { 22 | const providers: { 23 | [key: string]: React.ComponentType; 24 | }; 25 | export = providers; 26 | } 27 | 28 | declare module 'Routes' { 29 | const routes: { 30 | [key: string]: React.ComponentType; 31 | }; 32 | export = routes; 33 | } 34 | 35 | declare module 'Utils' { 36 | const utils: { 37 | [key: string]: any; 38 | }; 39 | export = utils; 40 | } 41 | 42 | // Path-based type declarations 43 | declare module '@/*' { 44 | const content: any; 45 | export default content; 46 | } 47 | 48 | declare module '@Components/*' { 49 | const content: React.ComponentType; 50 | export default content; 51 | } 52 | 53 | declare module '@Routes/*' { 54 | const content: React.ComponentType; 55 | export default content; 56 | } 57 | 58 | declare module '@Locales/*' { 59 | const content: any; 60 | export default content; 61 | } 62 | 63 | declare module '@Providers/*' { 64 | const content: React.ComponentType; 65 | export default content; 66 | } 67 | 68 | declare module '@Utils/*' { 69 | const content: any; 70 | export default content; 71 | } -------------------------------------------------------------------------------- /.playwright.config.txt: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | const config: PlaywrightTestConfig = { 13 | testDir: "./e2e", 14 | /* Maximum time one test can run for. */ 15 | timeout: 30 * 1000, 16 | expect: { 17 | /** 18 | * Maximum time expect() should wait for the condition to be met. 19 | * For example in `await expect(locator).toHaveText();` 20 | */ 21 | timeout: 5000, 22 | }, 23 | /* Run tests in files in parallel */ 24 | fullyParallel: true, 25 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 26 | forbidOnly: !!process.env.CI, 27 | /* Retry on CI only */ 28 | retries: process.env.CI ? 2 : 0, 29 | /* Opt out of parallel tests on CI. */ 30 | workers: process.env.CI ? 1 : undefined, 31 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 32 | reporter: "html", 33 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 34 | use: { 35 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 36 | actionTimeout: 0, 37 | /* Base URL to use in actions like `await page.goto('/')`. */ 38 | // baseURL: 'http://localhost:3000', 39 | 40 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 41 | trace: "on-first-retry", 42 | }, 43 | 44 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 45 | // outputDir: 'test-results/', 46 | 47 | /* Run your local dev server before starting the tests */ 48 | // webServer: { 49 | // command: 'npm run start', 50 | // port: 3000, 51 | // }, 52 | }; 53 | 54 | export default config; 55 | -------------------------------------------------------------------------------- /src/Providers/StartupProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback, createContext, useContext } from 'react'; 2 | 3 | const NAV_CLOSED_CLASS = 'nav_closed'; 4 | 5 | interface NavContextType { 6 | isNavOpen: boolean; 7 | toggleNav: () => void; 8 | setNavOpen: (open: boolean) => void; 9 | } 10 | 11 | const NavContext = createContext(undefined); 12 | 13 | export const useNav = () => { 14 | const ctx = useContext(NavContext); 15 | if (!ctx) throw new Error('useNav must be used within a StartupProvider'); 16 | return ctx; 17 | }; 18 | 19 | const getInitialNavState = () => { 20 | const navStatus = localStorage.getItem('nav_status'); 21 | return navStatus !== 'closed'; 22 | }; 23 | 24 | const StartupProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 25 | const [isNavOpen, setIsNavOpen] = useState(getInitialNavState); 26 | 27 | useEffect(() => { 28 | if (!isNavOpen) { 29 | document.body.classList.add(NAV_CLOSED_CLASS); 30 | localStorage.setItem('nav_status', 'closed'); 31 | } else { 32 | document.body.classList.remove(NAV_CLOSED_CLASS); 33 | localStorage.setItem('nav_status', 'opened'); 34 | } 35 | }, [isNavOpen]); 36 | 37 | useEffect(() => { 38 | const onStorage = (e: StorageEvent) => { 39 | if (e.key === 'nav_status') { 40 | setIsNavOpen(e.newValue !== 'closed'); 41 | } 42 | }; 43 | window.addEventListener('storage', onStorage); 44 | return () => window.removeEventListener('storage', onStorage); 45 | }, []); 46 | 47 | const toggleNav = useCallback(() => setIsNavOpen(open => !open), []); 48 | const setNavOpen = useCallback((open: boolean) => setIsNavOpen(open), []); 49 | 50 | return ( 51 | 52 | {children} 53 | 54 | ); 55 | }; 56 | 57 | export default StartupProvider; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arocrypt", 3 | "version": "0.10.1", 4 | "main": "dist-electron/main/index.js", 5 | "description": "Strong encryption & steganography made simple — secure your data effortlessly with AroCodes’ trusted toolkit.", 6 | "homepage": "https://arocrypt.vercel.app", 7 | "author": { 8 | "name": "AroCodes", 9 | "email": "arocodes@gmail.com" 10 | }, 11 | "private": false, 12 | "debug": { 13 | "env": { 14 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" 15 | } 16 | }, 17 | "type": "module", 18 | "scripts": { 19 | "dev": "vite", 20 | "build": "tsc && vite build && node build.js", 21 | "preview": "vite preview", 22 | "pretest": "vite build --mode=test", 23 | "test": "vitest run" 24 | }, 25 | "dependencies": { 26 | "@journeyapps/sqlcipher": "^5.3.1", 27 | "dotenv": "^16.4.7", 28 | "electron-store": "^10.0.0", 29 | "electron-updater": "^6.3.9", 30 | "fs-extra": "^11.3.0", 31 | "keytar": "^7.9.0", 32 | "ldrs": "^1.1.7", 33 | "mlkem": "^2.5.0", 34 | "pngjs": "^7.0.0", 35 | "react-haiku": "^2.3.0", 36 | "uuid": "^11.1.0" 37 | }, 38 | "devDependencies": { 39 | "@playwright/test": "^1.48.2", 40 | "@reduxjs/toolkit": "^2.5.0", 41 | "@types/node": "^22.10.5", 42 | "@types/pngjs": "^6.0.5", 43 | "@types/react": "^18.3.18", 44 | "@types/react-dom": "^18.3.1", 45 | "@vitejs/plugin-react": "^4.3.3", 46 | "autoprefixer": "^10.4.20", 47 | "electron": "^39.2.4", 48 | "electron-builder": "^26.0.12", 49 | "i18next": "^25.4.1", 50 | "lucide-react": "^0.474.0", 51 | "postcss": "^8.4.49", 52 | "postcss-import": "^16.1.0", 53 | "react": "^18.3.1", 54 | "react-dom": "^18.3.1", 55 | "react-i18next": "^15.4.0", 56 | "react-redux": "^9.2.0", 57 | "react-router-dom": "^7.1.1", 58 | "tailwindcss": "^3.4.15", 59 | "typescript": "^5.7.3", 60 | "vite": "^7.2.4", 61 | "vite-plugin-electron": "^0.29.0", 62 | "vite-plugin-electron-renderer": "^0.14.6", 63 | "vitest": "^4.0.14" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | useState, 5 | useEffect, 6 | ReactNode, 7 | } from 'react'; 8 | 9 | const themeTypes = ['dark', 'light'] as const; 10 | export type ThemeType = typeof themeTypes[number]; 11 | 12 | interface ThemeContextType { 13 | themeType: ThemeType; 14 | setThemeType: (type: ThemeType) => void; 15 | theme: ThemeType; 16 | themeName: ThemeType; 17 | } 18 | 19 | const defaultType: ThemeType = 'light'; 20 | 21 | const ThemeContext = createContext({ 22 | themeType: defaultType, 23 | setThemeType: () => { }, 24 | theme: defaultType, 25 | themeName: defaultType, 26 | }); 27 | 28 | export const useTheme = () => useContext(ThemeContext); 29 | 30 | function parseTheme(theme: string | null): ThemeType { 31 | if (themeTypes.includes(theme as ThemeType)) { 32 | return theme as ThemeType; 33 | } 34 | return getSystemPreferredTheme(); 35 | } 36 | 37 | function getSystemPreferredTheme(): ThemeType { 38 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 39 | } 40 | 41 | export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 42 | const [themeType, setThemeType] = useState(() => { 43 | return parseTheme(localStorage.getItem('theme')); 44 | }); 45 | 46 | const theme = themeType; 47 | const themeName = themeType; 48 | 49 | useEffect(() => { 50 | document.documentElement.setAttribute('data-theme', theme); 51 | localStorage.setItem('theme', theme); 52 | }, [theme]); 53 | 54 | return ( 55 | 56 | {children} 57 | 58 | ); 59 | }; 60 | 61 | export const initializeTheme = () => { 62 | const theme = parseTheme(localStorage.getItem('theme')); 63 | document.documentElement.setAttribute('data-theme', theme); 64 | localStorage.setItem('theme', theme); 65 | }; 66 | 67 | initializeTheme(); -------------------------------------------------------------------------------- /src/Context/DecryptContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useToast } from './ToastContext'; 5 | 6 | export type File = { 7 | path: string; 8 | name: string; 9 | }; 10 | 11 | type DecryptContextType = { 12 | files: File[]; 13 | setFiles: (files: File[]) => void; 14 | }; 15 | 16 | const DecryptContext = createContext(undefined); 17 | 18 | export const useDecrypt = () => { 19 | const ctx = useContext(DecryptContext); 20 | if (!ctx) throw new Error('useDecrypt must be used within DecryptProvider'); 21 | return ctx; 22 | }; 23 | 24 | export const DecryptProvider = ({ children }: { children: ReactNode }) => { 25 | const { t } = useTranslation(); 26 | const toast = useToast(); 27 | const [files, setFiles] = useState([]); 28 | const navigate = useNavigate(); 29 | const [isManyFiles, setIsManyFiles] = useState(false); 30 | 31 | useEffect(() => { 32 | window.electronAPI.onFilesToDecrypt((incomingFiles: string[]) => { 33 | setIsManyFiles(false); 34 | 35 | if (files.length > 0) { 36 | navigate('/decryption/file'); 37 | } 38 | 39 | setFiles(prevFiles => { 40 | const newFiles = incomingFiles.map(path => ({ 41 | path, 42 | name: path.split(/[\\/]/).pop() || path, 43 | })); 44 | const allFiles = [...prevFiles, ...newFiles]; 45 | const uniqueFiles = Array.from(new Map(allFiles.map(f => [f.path, f])).values()); 46 | 47 | if (uniqueFiles.length >= 21) { 48 | setIsManyFiles(true); 49 | return prevFiles; 50 | } 51 | 52 | return uniqueFiles; 53 | }); 54 | }); 55 | }, []); 56 | 57 | useEffect(() => { 58 | if (isManyFiles === true) { 59 | toast(2, t('toast.file_limit_msg'), t('toast.file_limit_title')); 60 | } 61 | }, [isManyFiles]) 62 | 63 | useEffect(() => { 64 | if (files.length > 0) { 65 | navigate('/decryption/file'); 66 | } 67 | }, [files]); 68 | 69 | return ( 70 | 71 | {children} 72 | 73 | ); 74 | }; -------------------------------------------------------------------------------- /src/Context/EncryptContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useToast } from './ToastContext'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | export type File = { 7 | path: string; 8 | name: string; 9 | }; 10 | 11 | type EncryptContextType = { 12 | files: File[]; 13 | setFiles: (files: File[]) => void; 14 | }; 15 | 16 | const EncryptContext = createContext(undefined); 17 | 18 | export const useEncrypt = () => { 19 | const ctx = useContext(EncryptContext); 20 | if (!ctx) throw new Error('useEncrypt must be used within EncryptProvider'); 21 | return ctx; 22 | }; 23 | 24 | export const EncryptProvider = ({ children }: { children: ReactNode }) => { 25 | const { t } = useTranslation(); 26 | const toast = useToast(); 27 | const [files, setFiles] = useState([]); 28 | const navigate = useNavigate(); 29 | const [isManyFiles, setIsManyFiles] = useState(false); 30 | 31 | useEffect(() => { 32 | window.electronAPI.onFilesToEncrypt((incomingFiles: string[]) => { 33 | setIsManyFiles(false); 34 | 35 | if (files.length > 0) { 36 | navigate('/encryption/file'); 37 | } 38 | 39 | setFiles(prevFiles => { 40 | const newFiles = incomingFiles.map(path => ({ 41 | path, 42 | name: path.split(/[\\/]/).pop() || path, 43 | })); 44 | const allFiles = [...prevFiles, ...newFiles]; 45 | const uniqueFiles = Array.from(new Map(allFiles.map(f => [f.path, f])).values()); 46 | 47 | if (uniqueFiles.length >= 21) { 48 | setIsManyFiles(true); 49 | return prevFiles; 50 | } 51 | 52 | return uniqueFiles; 53 | }); 54 | }); 55 | }, []); 56 | 57 | useEffect(() => { 58 | if (isManyFiles === true) { 59 | toast(2, t('toast.file_limit_msg'), t('toast.file_limit_title')); 60 | } 61 | }, [isManyFiles]) 62 | 63 | useEffect(() => { 64 | if (files.length > 0) { 65 | navigate('/encryption/file'); 66 | } 67 | }, [files]); 68 | 69 | return ( 70 | 71 | {children} 72 | 73 | ); 74 | }; -------------------------------------------------------------------------------- /src/Providers/RippleProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | const RippleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 4 | useEffect(() => { 5 | function addRippleEffect(element: HTMLElement) { 6 | element.removeEventListener("pointerdown", handleRipple); 7 | element.addEventListener("pointerdown", handleRipple); 8 | } 9 | 10 | function handleRipple(mouseEvent: PointerEvent) { 11 | const element = mouseEvent.currentTarget as HTMLElement; 12 | var rect = element.getBoundingClientRect(); 13 | var { clientX, clientY } = mouseEvent; 14 | var x = clientX - rect.left; 15 | var y = clientY - rect.top; 16 | var rippleEl = createRippleElement(x, y); 17 | element.appendChild(rippleEl); 18 | requestAnimationFrame(() => rippleEl.classList.add("run")); 19 | rippleEl.addEventListener("transitionend", () => rippleEl.remove()); 20 | } 21 | 22 | function createRippleElement(x: number, y: number) { 23 | var rippleEl = document.createElement("div"); 24 | rippleEl.classList.add("ripple"); 25 | rippleEl.style.left = `${x}px`; 26 | rippleEl.style.top = `${y}px`; 27 | return rippleEl; 28 | } 29 | 30 | function applyRippleEffects() { 31 | const elementsWithRipple = document.querySelectorAll(".re"); 32 | elementsWithRipple.forEach((elementWithRipple) => 33 | addRippleEffect(elementWithRipple as HTMLElement) 34 | ); 35 | } 36 | 37 | applyRippleEffects(); 38 | 39 | const observer = new MutationObserver(applyRippleEffects); 40 | observer.observe(document.body, { 41 | childList: true, 42 | subtree: true 43 | }); 44 | 45 | return () => { 46 | observer.disconnect(); 47 | const elementsWithRipple = document.querySelectorAll(".re"); 48 | elementsWithRipple.forEach((element) => { 49 | element.removeEventListener('pointerdown', handleRipple as EventListener); 50 | }); 51 | }; 52 | }, []); 53 | 54 | return <>{children}; 55 | }; 56 | 57 | export default RippleProvider; 58 | -------------------------------------------------------------------------------- /src/Routes/components/SettingsNav.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { Link, useLocation } from "react-router-dom" 3 | 4 | const SettingsNav = () => { 5 | const { t } = useTranslation(); 6 | const location = useLocation(); 7 | const isSettingsPage = window.location.hash.startsWith("#/settings"); 8 | 9 | return ( 10 |
11 |
12 | 16 |
17 | 18 | {t('security')} 19 |
20 | 21 | 25 |
26 | 27 | {t('appearance')} 28 |
29 | 30 |
31 |
32 | ) 33 | } 34 | 35 | export default SettingsNav -------------------------------------------------------------------------------- /.vite.config.flat.txt: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'node:fs' 2 | import path from 'node:path' 3 | import { defineConfig } from 'vite' 4 | import react from '@vitejs/plugin-react' 5 | import electron from 'vite-plugin-electron' 6 | import renderer from 'vite-plugin-electron-renderer' 7 | import pkg from './package.json' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig(({ command }) => { 11 | rmSync('dist-electron', { recursive: true, force: true }) 12 | 13 | const isServe = command === 'serve' 14 | const isBuild = command === 'build' 15 | const sourcemap = isServe || !!process.env.VSCODE_DEBUG 16 | 17 | return { 18 | resolve: { 19 | alias: { 20 | '@': path.join(__dirname, 'src') 21 | }, 22 | }, 23 | plugins: [ 24 | react(), 25 | electron([ 26 | { 27 | // Main-Process entry file of the Electron App. 28 | entry: 'electron/main/index.ts', 29 | onstart(options) { 30 | if (process.env.VSCODE_DEBUG) { 31 | console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') 32 | } else { 33 | options.startup() 34 | } 35 | }, 36 | vite: { 37 | build: { 38 | sourcemap, 39 | minify: isBuild, 40 | outDir: 'dist-electron/main', 41 | rollupOptions: { 42 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 43 | }, 44 | }, 45 | }, 46 | }, 47 | { 48 | entry: 'electron/preload/index.ts', 49 | onstart(options) { 50 | // Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete, 51 | // instead of restarting the entire Electron App. 52 | options.reload() 53 | }, 54 | vite: { 55 | build: { 56 | sourcemap: sourcemap ? 'inline' : undefined, // #332 57 | minify: isBuild, 58 | outDir: 'dist-electron/preload', 59 | rollupOptions: { 60 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 61 | }, 62 | }, 63 | }, 64 | } 65 | ]), 66 | // Use Node.js API in the Renderer-process 67 | renderer(), 68 | ], 69 | server: process.env.VSCODE_DEBUG && (() => { 70 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) 71 | return { 72 | host: url.hostname, 73 | port: +url.port, 74 | } 75 | })(), 76 | clearScreen: false, 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /electron/main/utils/KeyService.ts: -------------------------------------------------------------------------------- 1 | import { safeWriteLog } from './writeLog'; 2 | import { MlKem768 } from 'mlkem'; 3 | import { saveKEMKeyToDB, getKeys } from './db/DBService'; 4 | 5 | export function validateKemKey(key: string, type: "public" | "secret"): boolean { 6 | if (!key || typeof key !== "string") return false; 7 | 8 | try { 9 | const bytes = Buffer.from(key, "base64"); 10 | if (type === "public") return bytes.length > 0; 11 | if (type === "secret") return bytes.length > 0; 12 | return false; 13 | } catch { 14 | return false; 15 | } 16 | } 17 | 18 | export async function saveKEMKey(public_key: string, secret_key: string, recipient_key: string): Promise { 19 | try { 20 | if (!validateKemKey(secret_key, "secret") || !validateKemKey(public_key, "public") || !validateKemKey(recipient_key, "public")) { 21 | safeWriteLog('[KeyService] Invalid KEM key format detected - keys must be valid base64 with correct byte length'); 22 | return false; 23 | } 24 | 25 | await saveKEMKeyToDB(public_key, secret_key, recipient_key); 26 | return true; 27 | } catch (error) { 28 | safeWriteLog(`[KeyService] Error Saving KEM Keys: ${error instanceof Error ? error.message : 'Unknown error'}`); 29 | return false; 30 | } 31 | } 32 | 33 | // Post-Quantum Key Exchange 34 | 35 | export type MlKemKeyPair = { 36 | publicKey: string; // base64 37 | secretKey: string; // base64 38 | }; 39 | 40 | export async function createMlKemKeys(): Promise { 41 | const kem = new MlKem768(); 42 | 43 | // Generate key pair 44 | const [pk, sk] = await kem.generateKeyPair(); 45 | 46 | // Convert to base64 for IPC / storage 47 | return { 48 | publicKey: Buffer.from(pk).toString("base64"), 49 | secretKey: Buffer.from(sk).toString("base64"), 50 | }; 51 | } 52 | 53 | export type KemKeys = { 54 | PRIVATE_KEY: string; 55 | PUBLIC_KEY: string; 56 | RECIPIENT_KEY: string; 57 | }; 58 | 59 | export async function loadKemKeys(): Promise { 60 | const keys = await getKeys(); 61 | if (!keys || keys.length === 0) { 62 | throw new Error("[KEYS] No KEM keys found!"); 63 | } 64 | 65 | const PRIVATE_KEY = keys[0].secret; 66 | const PUBLIC_KEY = keys[0].public; 67 | const RECIPIENT_KEY = keys[0].recipient; 68 | 69 | if (!PRIVATE_KEY) throw new Error("[KEYS] PRIVATE_KEY not found!"); 70 | if (!PUBLIC_KEY) throw new Error("[KEYS] PUBLIC_KEY not found!"); 71 | if (!RECIPIENT_KEY) throw new Error("[KEYS] RECIPIENT_KEY not found!"); 72 | 73 | return { PRIVATE_KEY, PUBLIC_KEY, RECIPIENT_KEY }; 74 | } -------------------------------------------------------------------------------- /electron/main/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | 3 | type KeyParams = { originalKey: string; method: string; salt: Buffer }; 4 | 5 | export function generateKey({ originalKey, method, salt }: KeyParams) { 6 | if (!originalKey) throw new Error("No encryption key provided"); 7 | 8 | const iterations = 100000; 9 | let keyLength: number; 10 | 11 | switch (method.toUpperCase()) { 12 | case "AES-256-CBC": 13 | case "AES-256-GCM": 14 | case "AES-256-CTR": 15 | keyLength = 32; 16 | break; 17 | case "AES-192-CBC": 18 | case "AES-192-GCM": 19 | case "AES-192-CTR": 20 | keyLength = 24; 21 | break; 22 | case "AES-128-CBC": 23 | case "AES-128-GCM": 24 | case "AES-128-CTR": 25 | keyLength = 16; 26 | break; 27 | 28 | default: 29 | throw new Error(`Algorithm "${method}" not found!`); 30 | } 31 | 32 | return crypto.pbkdf2Sync(originalKey, salt, iterations, keyLength, "sha256"); 33 | } 34 | 35 | export function getIVLength(method: string): number { 36 | switch (method.toUpperCase()) { 37 | case "AES-128-CBC": 38 | case "AES-192-CBC": 39 | case "AES-256-CBC": 40 | case "AES-128-CTR": 41 | case "AES-192-CTR": 42 | case "AES-256-CTR": 43 | return 16; 44 | 45 | case "AES-128-GCM": 46 | case "AES-192-GCM": 47 | case "AES-256-GCM": 48 | return 12; 49 | 50 | default: 51 | throw new Error(`IV length for algorithm "${method}" not found!`); 52 | } 53 | } 54 | 55 | export function AesKeySlice(algorithm: string, aesKey: Buffer): Buffer { 56 | let requiredLength: number; 57 | 58 | switch (algorithm.toUpperCase()) { 59 | case "AES-128-CBC": 60 | case "AES-128-CTR": 61 | case "AES-128-GCM": 62 | requiredLength = 16; 63 | break; 64 | case "AES-192-CBC": 65 | case "AES-192-CTR": 66 | case "AES-192-GCM": 67 | requiredLength = 24; 68 | break; 69 | case "AES-256-CBC": 70 | case "AES-256-CTR": 71 | case "AES-256-GCM": 72 | requiredLength = 32; 73 | break; 74 | default: 75 | throw new Error(`Unsupported algorithm: ${algorithm}`); 76 | } 77 | 78 | if (aesKey.length < requiredLength) { 79 | const padded = Buffer.alloc(requiredLength); 80 | aesKey.copy(padded); 81 | return padded; 82 | } 83 | 84 | if (aesKey.length > requiredLength) { 85 | return aesKey.slice(0, requiredLength); 86 | } 87 | 88 | return aesKey; 89 | } -------------------------------------------------------------------------------- /src/Components/SettingsDropDown.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect, useRef } from "react"; 2 | 3 | type Props = { 4 | label: string; 5 | isOpen: boolean; 6 | setIsOpen: (value: boolean) => void; 7 | dropdownRef?: React.RefObject; 8 | onToggle?: () => void; 9 | children: ReactNode; 10 | }; 11 | 12 | const SettingsDropDown: React.FC = ({ 13 | label, 14 | isOpen, 15 | setIsOpen, 16 | dropdownRef, 17 | onToggle, 18 | children 19 | }) => { 20 | const handleClick = (e: React.MouseEvent) => { 21 | e.preventDefault(); 22 | e.stopPropagation(); 23 | setIsOpen(!isOpen); 24 | if (onToggle) onToggle(); 25 | }; 26 | 27 | const containerRef = useRef(null); 28 | 29 | useEffect(() => { 30 | const handleClickOutside = (event: MouseEvent) => { 31 | if ( 32 | isOpen && 33 | containerRef.current && 34 | !containerRef.current.contains(event.target as Node) 35 | ) { 36 | setIsOpen(false); 37 | } 38 | }; 39 | 40 | document.addEventListener("mousedown", handleClickOutside); 41 | return () => { 42 | document.removeEventListener("mousedown", handleClickOutside); 43 | }; 44 | }, [isOpen, setIsOpen]); 45 | 46 | const handleToggle = (e: React.MouseEvent) => { 47 | e.preventDefault(); 48 | e.stopPropagation(); 49 | setIsOpen(!isOpen); 50 | }; 51 | 52 | return ( 53 |
54 | 75 |
79 | {children} 80 |
81 |
82 | ); 83 | }; 84 | 85 | export default SettingsDropDown; 86 | -------------------------------------------------------------------------------- /src/Routes/tabs/Appearance.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useAppDispatch } from "@/store"; 3 | import { useTranslation } from "react-i18next"; 4 | import BottomInfo from "@/Components/BottomInfo"; 5 | import SettingsDropDown from "@/Components/SettingsDropDown"; 6 | import { useSelector } from "react-redux"; 7 | import { setLanguage } from "@/globalSlice"; 8 | import ThemeCustomBox from "../components/ThemeCustomBox"; 9 | 10 | const AppearanceSettings = () => { 11 | const { t } = useTranslation(); 12 | const dispatch = useAppDispatch(); 13 | 14 | /* info: States */ 15 | const [isLangMenuOpen, setIsLangMenuOpen] = useState(false); 16 | 17 | const currentLanguage = useSelector((state: any) => state.global.language); 18 | const handleLanguageChange = (lang: any) => { 19 | dispatch(setLanguage(lang)); 20 | }; 21 | 22 | return ( 23 | <> 24 |
25 |
26 |
27 |

{t("interface_language")}

28 |

{t("interface_language_info")}

29 |
30 | 35 | 41 | 47 | 48 |
49 |
50 |
51 |

{t("app_theme.title")}

52 |

{t("app_theme.desc")}

53 |
54 |
55 | 56 | 57 |
58 |
59 | 60 |
61 | 62 | ); 63 | }; 64 | 65 | export default AppearanceSettings; 66 | -------------------------------------------------------------------------------- /electron/main/utils/validateFileDecryption.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import crypto from "crypto"; 3 | import { generateKey, getIVLength } from "./crypto"; 4 | import { safeWriteLog } from "./writeLog"; 5 | import { loadKemKeys } from "./KeyService"; 6 | 7 | export async function validateFileDecryption( 8 | inputPath: string, 9 | method: string 10 | ): Promise { 11 | if (!inputPath || !method) { 12 | throw new Error('Invalid input parameters for file decryption validation'); 13 | } 14 | 15 | try { 16 | const { PRIVATE_KEY } = await loadKemKeys(); 17 | if (!PRIVATE_KEY) throw new Error('No PRIVATE_KEY found'); 18 | 19 | const fileBuffer = await fs.promises.readFile(inputPath); 20 | const fileStats = await fs.promises.stat(inputPath); 21 | 22 | const ivLength = getIVLength(method); 23 | const ivBuffer = fileBuffer.slice(0, ivLength); 24 | const saltBuffer = fileBuffer.slice(ivLength, ivLength + 16); 25 | 26 | safeWriteLog(`[VALIDATE] IV (hex): ${ivBuffer.toString('hex')}`); 27 | safeWriteLog(`[VALIDATE] Salt (hex): ${saltBuffer.toString('hex')}`); 28 | 29 | const keyBuffer = generateKey({ originalKey: PRIVATE_KEY, method, salt: saltBuffer }); 30 | safeWriteLog(`[VALIDATE] Generated key length: ${keyBuffer.length} bytes`); 31 | 32 | if (!/gcm|chacha/i.test(method)) { 33 | // HMAC verification for non-GCM/ChaCha 34 | const hmacStart = fileStats.size - 32; 35 | const encryptedContent = fileBuffer.slice(ivLength + 16, hmacStart); 36 | const storedHmac = fileBuffer.slice(hmacStart); 37 | 38 | const hmacKey = crypto.createHash('sha256').update(keyBuffer).digest(); 39 | const hmac = crypto.createHmac('sha256', hmacKey); 40 | hmac.update(ivBuffer); 41 | hmac.update(saltBuffer); 42 | hmac.update(encryptedContent); 43 | 44 | const calculatedHmac = hmac.digest(); 45 | if (!calculatedHmac.equals(storedHmac)) { 46 | safeWriteLog(`[VALIDATE] HMAC verification failed`); 47 | return 'bad_validate'; 48 | } 49 | 50 | safeWriteLog(`[VALIDATE] HMAC verification successful`); 51 | 52 | // Optional: try decryption to ensure key correctness 53 | const decipher = crypto.createDecipheriv(method, keyBuffer, ivBuffer); 54 | decipher.update(encryptedContent); 55 | decipher.final(); 56 | } else { 57 | safeWriteLog(`[VALIDATE] GCM/ChaCha mode detected: skipping HMAC validation`); 58 | // In GCM/ChaCha you should validate separately with auth tag during actual decryption 59 | } 60 | 61 | return 'ok'; 62 | } catch (error: any) { 63 | await safeWriteLog(`[VALIDATE] Decryption validation failed: ${error.message || 'Unknown error'}`); 64 | return 'bad_validate'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { rmSync } from 'node:fs' 2 | import path from 'node:path' 3 | import { defineConfig } from 'vite' 4 | import react from '@vitejs/plugin-react' 5 | import electron from 'vite-plugin-electron/simple' 6 | import pkg from './package.json' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(({ command }) => { 10 | rmSync('dist-electron', { recursive: true, force: true }) 11 | 12 | const isServe = command === 'serve' 13 | const isBuild = command === 'build' 14 | const sourcemap = isServe || !!process.env.VSCODE_DEBUG 15 | 16 | return { 17 | resolve: { 18 | alias: { 19 | '@': path.join(__dirname, 'src'), 20 | '@Components': path.resolve(__dirname, 'src/Components'), 21 | '@Routes': path.resolve(__dirname, 'src/Routes'), 22 | '@Locales': path.resolve(__dirname, 'src/Locales'), 23 | '@Providers': path.resolve(__dirname, 'src/Providers'), 24 | '@Utils': path.resolve(__dirname, 'src/Utils') 25 | }, 26 | }, 27 | plugins: [ 28 | react(), 29 | electron({ 30 | main: { 31 | // Shortcut of `build.lib.entry` 32 | entry: 'electron/main/index.ts', 33 | onstart(args) { 34 | if (process.env.VSCODE_DEBUG) { 35 | console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App') 36 | } else { 37 | args.startup() 38 | } 39 | }, 40 | vite: { 41 | build: { 42 | sourcemap, 43 | minify: isBuild, 44 | outDir: 'dist-electron/main', 45 | rollupOptions: { 46 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 47 | }, 48 | }, 49 | }, 50 | }, 51 | preload: { 52 | // Shortcut of `build.rollupOptions.input`. 53 | // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. 54 | input: 'electron/preload/index.ts', 55 | vite: { 56 | build: { 57 | sourcemap: sourcemap ? 'inline' : undefined, // #332 58 | minify: isBuild, 59 | outDir: 'dist-electron/preload', 60 | rollupOptions: { 61 | external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}), 62 | }, 63 | }, 64 | }, 65 | }, 66 | // Ployfill the Electron and Node.js API for Renderer process. 67 | // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process. 68 | // See https://github.com/electron-vite/vite-plugin-electron-renderer 69 | renderer: {}, 70 | }), 71 | ], 72 | server: process.env.VSCODE_DEBUG && (() => { 73 | const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL) 74 | return { 75 | host: url.hostname, 76 | port: +url.port, 77 | } 78 | })(), 79 | clearScreen: false, 80 | } 81 | }) 82 | -------------------------------------------------------------------------------- /src/Components/ModalAlertBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | interface ModalAlertBoxProps { 4 | header: string; 5 | text_info: string; 6 | type: number; 7 | changeable: boolean; 8 | } 9 | 10 | const ModalAlertBox: React.FC = ({ header, text_info, type, changeable }) => { 11 | const [isInfoBlockOpen, setIsInfoBlockOpen] = useState(false); 12 | 13 | const toggleBox = () => { 14 | if (changeable) { 15 | setIsInfoBlockOpen(!isInfoBlockOpen); 16 | } 17 | } 18 | 19 | return ( 20 |
24 |
25 |
26 |

27 | { 28 | type === 0 ? ( 29 | 30 | ) : ( 31 | 32 | ) 33 | } 34 | {header} 35 |

36 | { 37 | changeable && ( 38 |
39 | 40 |
41 | ) 42 | } 43 |
44 |

{text_info}

45 |
46 |
47 | ) 48 | } 49 | 50 | export default ModalAlertBox -------------------------------------------------------------------------------- /electron/main/utils/encryptText.ts: -------------------------------------------------------------------------------- 1 | import { AesKeySlice, generateKey, getIVLength } from "./crypto"; 2 | import crypto from "crypto"; 3 | import { MlKem768 } from "mlkem"; 4 | import { loadKemKeys } from "./KeyService"; 5 | import { safeWriteLog } from "./writeLog"; 6 | 7 | const kem = new MlKem768(); 8 | 9 | interface EncryptedPayload { 10 | content: string; 11 | iv: string; 12 | salt: string; 13 | kemCiphertext: string | null; 14 | hmac?: string; 15 | authTag?: string; 16 | } 17 | 18 | export async function encrypt( 19 | text: string, 20 | method: string, 21 | isShareable: boolean 22 | ): Promise { 23 | safeWriteLog(`[ENCRYPT] Starting Text Encryption, isShareable: ${isShareable}`); 24 | 25 | const { PUBLIC_KEY, RECIPIENT_KEY } = await loadKemKeys(); 26 | const ivLength = getIVLength(method); 27 | const iv = crypto.randomBytes(ivLength); 28 | const salt = crypto.randomBytes(16); 29 | 30 | let aesKey: Buffer; 31 | let kemCiphertext: string | null = null; 32 | 33 | if (!ivLength) throw new Error("Invalid IV length for selected algorithm."); 34 | if ((isShareable && !RECIPIENT_KEY) || (!isShareable && !PUBLIC_KEY)) throw new Error("PROBLEM WITH KEYS!."); 35 | 36 | if (isShareable) { 37 | const recipientKeyUint8 = Uint8Array.from(Buffer.from(RECIPIENT_KEY, "base64")); 38 | const [ciphertext, sharedSecret] = await kem.encap(recipientKeyUint8); 39 | kemCiphertext = Buffer.from(ciphertext).toString("base64"); 40 | 41 | aesKey = generateKey({ 42 | originalKey: Buffer.from(sharedSecret).toString(), 43 | method, 44 | salt, 45 | }); 46 | } else { 47 | const publicKeyUint8 = Uint8Array.from(Buffer.from(PUBLIC_KEY, "base64")); 48 | const [ciphertext, sharedSecret] = await kem.encap(publicKeyUint8); 49 | kemCiphertext = Buffer.from(ciphertext).toString("base64"); 50 | 51 | aesKey = generateKey({ 52 | originalKey: Buffer.from(sharedSecret).toString(), 53 | method, 54 | salt, 55 | }); 56 | } 57 | 58 | const key = AesKeySlice(method, aesKey); 59 | const cipher = crypto.createCipheriv(method, key, iv); 60 | 61 | let encrypted = cipher.update(text, "utf8", "hex"); 62 | encrypted += cipher.final("hex"); 63 | 64 | const payload: EncryptedPayload = { 65 | content: encrypted, 66 | iv: iv.toString("hex"), 67 | salt: salt.toString("hex"), 68 | kemCiphertext, 69 | }; 70 | 71 | // AEAD (GCM, ChaCha, etc.) → capture auth tag 72 | if (/gcm|chacha/i.test(method)) { 73 | const tag = (cipher as crypto.CipherGCM).getAuthTag(); 74 | payload.authTag = tag.toString("hex"); 75 | } else { 76 | const hmacKey = crypto.createHash("sha256").update(key).digest(); 77 | const hmac = crypto.createHmac("sha256", hmacKey); 78 | hmac.update(iv); 79 | hmac.update(salt); 80 | hmac.update(Buffer.from(encrypted, "hex")); 81 | if (kemCiphertext) hmac.update(kemCiphertext); 82 | payload.hmac = hmac.digest("hex"); 83 | } 84 | 85 | const packed = Buffer.from(JSON.stringify(payload)).toString("base64"); 86 | safeWriteLog(`[ENCRYPT] Packed payload Base64: ${packed}`); 87 | 88 | return packed; 89 | } -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", 3 | "appId": "com.arocodes.arocrypt", 4 | "productName": "AroCrypt", 5 | "copyright": "Copyright 2025 AroCodes", 6 | "asar": { 7 | "smartUnpack": true 8 | }, 9 | "asarUnpack": [ 10 | "certs/arocrypt.pfx", 11 | "temp-electron-builder.json", 12 | ".env" 13 | ], 14 | "directories": { 15 | "output": "releases/${version}" 16 | }, 17 | "publish": [ 18 | { 19 | "provider": "github", 20 | "owner": "AroCrypt", 21 | "repo": "app", 22 | "private": false 23 | } 24 | ], 25 | "files": [ 26 | "!src/**", 27 | "!releases/**", 28 | "!logs/**", 29 | "!**/*.map", 30 | "!**/*.ts", 31 | "!**/*.tsx", 32 | "!**/*.md", 33 | "!**/__tests__/**", 34 | "!**/*.test.*", 35 | "!**/*.spec.*", 36 | "!**/test/**", 37 | "!**/example/**", 38 | "!**/docs/**", 39 | "!**/node_modules/**/*.md", 40 | "!**/node_modules/**/*.ts", 41 | "!**/node_modules/**/test/**", 42 | "dist-electron/**/*", 43 | "dist/**/*", 44 | "public/**/*", 45 | "assets/**/*" 46 | ], 47 | "win": { 48 | "target": [ 49 | "nsis", 50 | "msi", 51 | "portable" 52 | ], 53 | "icon": "assets/images/app-icons/win/icon.ico", 54 | "cscLink": "./certs/arocrypt.pfx", 55 | "cscKeyPassword": "${CERT_PASSWORD}", 56 | "forceCodeSigning": true 57 | }, 58 | "linux": { 59 | "category": "Utility", 60 | "icon": "assets/images/app-icons/png", 61 | "target": [ 62 | "deb", 63 | "AppImage" 64 | ] 65 | }, 66 | "mac": { 67 | "icon": "assets/images/app-icons/mac/icon.icns", 68 | "target": [ 69 | "dmg" 70 | ] 71 | }, 72 | "nsis": { 73 | "oneClick": false, 74 | "perMachine": true, 75 | "allowToChangeInstallationDirectory": true, 76 | "deleteAppDataOnUninstall": true, 77 | "createDesktopShortcut": true, 78 | "createStartMenuShortcut": true, 79 | "installerIcon": "assets/images/setup-icons/win/icon.ico", 80 | "installerHeaderIcon": "assets/images/app-icons/win/icon.ico", 81 | "uninstallDisplayName": "AroCrypt", 82 | "include": "assets/scripts/config.nsi", 83 | "artifactName": "${productName}-Setup-${version}.${ext}", 84 | "installerSidebar": "assets/images/setup-icons/sidebar.bmp", 85 | "installerHeader": "assets/images/setup-icons/header.bmp", 86 | "uninstallerSidebar": "assets/images/setup-icons/sidebar.bmp" 87 | }, 88 | "portable": { 89 | "requestExecutionLevel": "admin", 90 | "artifactName": "${productName}-Portable-${version}.${ext}" 91 | }, 92 | "extraResources": [ 93 | { 94 | "from": "public/logo", 95 | "to": "logo", 96 | "filter": [ 97 | "**/*" 98 | ] 99 | }, 100 | { 101 | "from": "public/other", 102 | "to": "other_images", 103 | "filter": [ 104 | "**/*" 105 | ] 106 | }, 107 | { 108 | "from": "certs/arocrypt.crt", 109 | "to": "certs/arocrypt.crt" 110 | }, 111 | { 112 | "from": "assets", 113 | "to": "assets", 114 | "filter": [ 115 | "**/*" 116 | ] 117 | } 118 | ] 119 | } -------------------------------------------------------------------------------- /src/Components/BottomInfo.tsx: -------------------------------------------------------------------------------- 1 | import useAppVersion from "@/Utils/getAppVersion"; 2 | import useOpenLink from "@/Utils/openLink"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | const BottomInfo = () => { 6 | const { t } = useTranslation(); 7 | const appVersion = useAppVersion(); 8 | 9 | return ( 10 |
11 |
12 |

13 | useOpenLink("https://github.com/OfficialAroCodes")}>{t("developed_by")} AroCodes 14 | 16 | useOpenLink( 17 | "https://github.com/AroCrypt/app/releases" 18 | ) 19 | } 20 | className="small" 21 | > 22 | Beta v{appVersion} 23 | 24 |

25 |
26 | 29 | 32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default BottomInfo; 39 | -------------------------------------------------------------------------------- /electron/main/utils/decryptText.ts: -------------------------------------------------------------------------------- 1 | import { generateKey } from "./crypto"; 2 | import crypto from "crypto"; 3 | import { loadKemKeys } from "./KeyService"; 4 | import { MlKem768 } from "mlkem"; 5 | 6 | const kem = new MlKem768(); 7 | 8 | interface EncryptedPayload { 9 | content: string; 10 | iv: string; 11 | salt: string; 12 | kemCiphertext: string | null; 13 | hmac?: string; 14 | authTag?: string; 15 | } 16 | 17 | export async function decrypt( 18 | packedData: string, 19 | method: string 20 | ): Promise { 21 | console.log("[DECRYPT] Starting decryption"); 22 | 23 | const { PRIVATE_KEY } = await loadKemKeys(); 24 | if (!PRIVATE_KEY) throw new Error("[DECRYPT] Private key not found!"); 25 | 26 | try { 27 | const jsonStr = Buffer.from(packedData, "base64").toString("utf8"); 28 | const payload: EncryptedPayload = JSON.parse(jsonStr); 29 | 30 | console.log("[DECRYPT] Payload:", payload); 31 | 32 | const iv = Buffer.from(payload.iv, "hex"); 33 | const salt = Buffer.from(payload.salt, "hex"); 34 | const encryptedBuffer = Buffer.from(payload.content, "hex"); 35 | 36 | if (!payload.kemCiphertext) throw new Error("Missing KEM ciphertext!"); 37 | 38 | const kemCiphertext = Uint8Array.from(Buffer.from(payload.kemCiphertext, "base64")); 39 | const privateKeyUint8 = Uint8Array.from(Buffer.from(PRIVATE_KEY, "base64")); 40 | const sharedSecret = await kem.decap(kemCiphertext, privateKeyUint8); 41 | 42 | const aesKey = generateKey({ 43 | originalKey: Buffer.from(sharedSecret).toString(), 44 | method, 45 | salt, 46 | }); 47 | 48 | console.log("[DECRYPT] Derived AES key (hex):", aesKey.toString("hex")); 49 | 50 | const keySlice = aesKey.slice(0, aesKey.length > 32 ? 32 : aesKey.length); 51 | 52 | // --- Integrity check before decrypt --- 53 | if (/gcm|chacha/i.test(method)) { 54 | if (!payload.authTag) throw new Error("[DECRYPT] Missing authTag for AEAD mode!"); 55 | } else { 56 | if (!payload.hmac) throw new Error("[DECRYPT] Missing HMAC for non-AEAD mode!"); 57 | 58 | const hmacKey = crypto.createHash("sha256").update(keySlice).digest(); 59 | const hmac = crypto.createHmac("sha256", hmacKey); 60 | hmac.update(iv); 61 | hmac.update(salt); 62 | hmac.update(encryptedBuffer); 63 | if (payload.kemCiphertext) hmac.update(payload.kemCiphertext); 64 | 65 | const computed = hmac.digest("hex"); 66 | console.log("[DECRYPT] Computed HMAC:", computed); 67 | console.log("[DECRYPT] Provided HMAC:", payload.hmac); 68 | 69 | if (computed !== payload.hmac) { 70 | throw new Error("HMAC verification failed: data integrity compromised"); 71 | } 72 | } 73 | 74 | // --- Proceed with decryption --- 75 | const decipher = crypto.createDecipheriv(method, keySlice, iv); 76 | if (/gcm|chacha/i.test(method)) { 77 | (decipher as crypto.DecipherGCM).setAuthTag(Buffer.from(payload.authTag!, "hex")); 78 | } 79 | 80 | let decrypted = decipher.update(encryptedBuffer); 81 | decrypted = Buffer.concat([decrypted, decipher.final()]); 82 | 83 | const finalText = decrypted.toString("utf8"); 84 | return finalText; 85 | } catch (error) { 86 | console.error("[DECRYPT] Exception:", error); 87 | return "invalid"; 88 | } 89 | } -------------------------------------------------------------------------------- /src/electron.d.ts: -------------------------------------------------------------------------------- 1 | interface FileResult { 2 | inputPath: string; 3 | output: string; 4 | } 5 | 6 | type KyberKeyPair = { 7 | secretKey: string; 8 | publicKey: string; 9 | }; 10 | 11 | interface ElectronAPI { 12 | // User Keys 13 | getPrivateKey: () => Promise; 14 | getPublicKey: () => Promise; 15 | 16 | kyberKeyPair: () => Promise; 17 | saveKeys: (secret_key: string, public_key: string, recipient_key: string) => Promise; 18 | getKeys: () => Promise<{ secret: string; public: string; recipient: string }>; 19 | noKeys: () => Promise; 20 | 21 | // Text 22 | encrypt: (params: { text: string; method: string; isSaveHistory: boolean; isShareable: boolean }) => Promise; 23 | decrypt: (params: { packedKeys: string; method: string; isSaveHistory: boolean }) => Promise; 24 | 25 | showSaveDialog: (options: { 26 | title?: string; 27 | defaultPath?: string; 28 | }) => Promise<{ 29 | canceled: boolean; 30 | filePath?: string; 31 | }>; 32 | 33 | onFilesToDecrypt: (callback: (paths: string[]) => void) => void; 34 | onFilesToEncrypt: (callback: (paths: string[]) => void) => void; 35 | 36 | encryptFile: ( 37 | filesPath: string[], 38 | method: string, 39 | isDeleteSource: boolean, 40 | isSaveHistory: boolean, 41 | isSingleOutput: boolean, 42 | isShareable: boolean 43 | ) => Promise; 44 | 45 | decryptFile: ( 46 | filesPath: string[], 47 | method: string, 48 | isDeleteSource: boolean, 49 | isSaveHistory: boolean, 50 | isSingleOutput: boolean 51 | ) => Promise; 52 | 53 | extractHiddenData: ( 54 | filesPath: string[], 55 | method: string, 56 | isDeleteSource: boolean, 57 | isSaveHistory: boolean, 58 | isSingleOutput: boolean 59 | ) => Promise; 60 | 61 | hideData: ( 62 | filesPath: string[], 63 | secretFilesPath: string[], 64 | method: string, 65 | isDeleteSource: boolean, 66 | isSaveHistory: boolean, 67 | isShareable: boolean 68 | ) => Promise; 69 | 70 | openFileDialog: () => Promise; 71 | openFileDialogD: () => Promise; 72 | 73 | // Embed Data 74 | selectDataHiderImage: () => Promise; 75 | selectDataHiderSecretFiles: () => Promise; 76 | 77 | selectDataExtractorImage: () => Promise; 78 | 79 | // Utility 80 | copyToClipboard: (text: string) => Promise; 81 | openExternalLink: (url: string) => void; 82 | getAppVersion: () => Promise; 83 | getPlatform: () => Promise; 84 | 85 | getLogs: (table: string) => Promise; 86 | deleteLog: (args: { table: string; id: string }) => Promise; 87 | 88 | // Window Control 89 | maximizeWindow: () => Promise; 90 | onMaximize: (callback: (isMax: boolean) => void) => void; 91 | minimizeWindow: () => Promise; 92 | closeWindow: () => Promise; 93 | 94 | // Update 95 | onUpdateAvailable: ( 96 | callback: (updateInfo: UpdateInfo & { isUpdateAvailable: boolean }) => void 97 | ) => void; 98 | onUpdateNotAvailable: ( 99 | callback: (updateInfo: UpdateInfo & { isUpdateAvailable: boolean }) => void 100 | ) => void; 101 | onUpdateDownloadProgress: ( 102 | callback: (progress: { 103 | percent: number; 104 | transferred: number; 105 | total: number; 106 | bytesPerSecond: number; 107 | }) => void 108 | ) => void; 109 | checkForUpdates: () => Promise; 110 | downloadUpdate?: () => Promise; 111 | openAboutWindow: () => Promise; 112 | } 113 | 114 | interface UpdateInfo { 115 | version: string; 116 | files: Array<{ 117 | url: string; 118 | size: number; 119 | }>; 120 | path?: string; 121 | releaseDate?: string; 122 | releaseNotes?: string; 123 | isUpdateAvailable?: boolean; 124 | } 125 | 126 | interface LogEntry { 127 | id: string; 128 | timestamp: number; 129 | input_path: string; 130 | output_path: string; 131 | input_size?: number; 132 | output_size?: number; 133 | status?: string; 134 | duration?: number; 135 | } 136 | 137 | interface Window { 138 | electronAPI: ElectronAPI; 139 | } 140 | -------------------------------------------------------------------------------- /src/Components/InlineMessageBox.tsx: -------------------------------------------------------------------------------- 1 | type InlineMessageBoxProps = { 2 | className?: string; 3 | message: string; 4 | type?: number; 5 | }; 6 | 7 | const InlineMessageBox = ({ className, message, type = 1 }: InlineMessageBoxProps) => { 8 | let msgType: string; 9 | 10 | switch (type) { 11 | case 2: 12 | msgType = "warn"; 13 | break; 14 | case 3: 15 | msgType = "error"; 16 | break; 17 | case 1: 18 | default: 19 | msgType = "info"; 20 | break; 21 | } 22 | 23 | return ( 24 |
25 | { 26 | msgType === 'warn' ? ( 27 | 28 | ) : msgType === 'error' ? 29 | ( 30 | 31 | ) : ( 32 | 33 | ) 34 | } 35 | {message} 36 |
37 | ); 38 | }; 39 | 40 | export default InlineMessageBox -------------------------------------------------------------------------------- /src/Components/ToastNotification.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | interface ToastInterface { 4 | type: number; 5 | message: string; 6 | title: string; 7 | onDone?: () => void; 8 | } 9 | 10 | const ToastNotification = ({ type, message, title, onDone }: ToastInterface) => { 11 | const [visible, setVisible] = useState(false); 12 | 13 | useEffect(() => { 14 | const showTimer = setTimeout(() => { 15 | setVisible(true); 16 | }, 10); 17 | 18 | const hideTimer = setTimeout(() => { 19 | setVisible(false); 20 | }, 4000); 21 | 22 | const cleanupTimer = setTimeout(() => { 23 | onDone?.(); 24 | }, 4300); 25 | 26 | return () => { 27 | clearTimeout(showTimer); 28 | clearTimeout(hideTimer); 29 | clearTimeout(cleanupTimer); 30 | }; 31 | }, []); 32 | 33 | return ( 34 |
35 | 36 | { 37 | type === 1 ? ( 38 | 39 | ) : type === 2 ? ( 40 | 41 | ) : ( 42 | 43 | ) 44 | } 45 | 46 |
47 |

{title}

48 |

{message}

49 |
50 |
51 | ); 52 | }; 53 | 54 | export default ToastNotification; 55 | -------------------------------------------------------------------------------- /src/Components/Titlebar.tsx: -------------------------------------------------------------------------------- 1 | import useAppVersion from '@/Utils/getAppVersion'; 2 | import React, { useState, useEffect, useRef } from 'react' 3 | import MainDropDown from './MainDropDown'; 4 | import { useClickOutside } from 'react-haiku'; 5 | 6 | interface TitlebarProps { 7 | isAbout: boolean; 8 | } 9 | 10 | const Titlebar: React.FC = ({ isAbout }) => { 11 | const appVersion = useAppVersion(); 12 | const [isMenuOpen, setIsMenuOpen] = useState(false); 13 | const [isMaximized, setIsMaximized] = useState(false); 14 | const dropdownRef = useRef(null); 15 | 16 | const handleMinimize = async () => { 17 | try { 18 | await window.electronAPI.minimizeWindow(); 19 | } catch (error) { 20 | console.error('Failed to minimize window:', error); 21 | } 22 | } 23 | 24 | const handleMaximize = async () => { 25 | try { 26 | await window.electronAPI.maximizeWindow(); 27 | } catch (error) { 28 | console.error('Failed to minimize window:', error); 29 | } 30 | } 31 | 32 | useEffect(() => { 33 | window.electronAPI.onMaximize((state) => { 34 | setIsMaximized(state); 35 | }); 36 | }, []); 37 | 38 | const handleClose = async () => { 39 | try { 40 | if (isAbout) { 41 | window.close() 42 | } else { 43 | await window.electronAPI.closeWindow(); 44 | } 45 | } catch (error) { 46 | console.error('Failed to close window:', error); 47 | } 48 | } 49 | 50 | useClickOutside(dropdownRef, () => setIsMenuOpen(false)); 51 | 52 | return ( 53 | <> 54 |
55 |
56 | AroCrypt Logo 57 |

AroCrypt v{appVersion}

58 |
59 |
60 | { 61 | !isAbout && ( 62 | <> 63 |
64 | 73 | 74 |
75 | 78 | 87 | 88 | ) 89 | } 90 | 93 |
94 |
95 | 96 | ) 97 | } 98 | 99 | export default Titlebar; -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { HashRouter, Routes, Route } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import store from '@/store'; 6 | import "@/main.css"; 7 | import '@/i18n'; 8 | 9 | /* info: Components */ 10 | import Titlebar from "@/Components/Titlebar"; 11 | import Navbar from '@/Components/Navbar'; 12 | import UpdateModal from '@/Components/UpdateModal'; 13 | import AboutWindow from '@/Components/AboutWindow'; 14 | 15 | /* info: Utils */ 16 | import Algorithm from './Utils/AlgorithmUtil'; 17 | 18 | /* info: Providers */ 19 | import { ThemeProvider, initializeTheme } from '@Providers/ThemeProvider'; 20 | import RippleProvider from '@/Providers/RippleProvider'; 21 | import StartupProvider from './Providers/StartupProvider'; 22 | import { DecryptProvider } from '@/Context/DecryptContext'; 23 | import { EncryptProvider } from '@/Context/EncryptContext'; 24 | import { ToastProvider } from '@/Context/ToastContext'; 25 | import { KeysMainProvider } from '@/Context/KeysContext'; 26 | import { ExtractProvider } from '@/Context/ExtractContext'; 27 | import { EmbedProvider } from '@/Context/EmbedContext'; 28 | 29 | /* info: Routes */ 30 | import FileEncryption from '@/Routes/FileEncryption'; 31 | import TextEncryption from "@/Routes/TextEncryption"; 32 | import TextDecryption from "@/Routes/TextDecryption"; 33 | import FileDecryption from '@/Routes/FileDecryption'; 34 | import Settings from '@/Routes/Settings'; 35 | import AppearanceSettings from '@/Routes/tabs/Appearance'; 36 | import SettingsNav from '@/Routes/components/SettingsNav'; 37 | import DataHider from '@/Routes/DataHider'; 38 | import DataExtractor from '@/Routes/DataExtracter'; 39 | 40 | 41 | initializeTheme(); 42 | 43 | const isAboutWindow = window.location.hash === '#about'; 44 | 45 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 46 | 47 | { 48 | isAboutWindow ? ( 49 | <> 50 | 51 | 52 | 53 | 54 | 55 | ) 56 | : ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | } /> 74 | {/* Encryption Routes */} 75 | } /> 76 | } /> 77 | 78 | {/* Decryption Routes */} 79 | } /> 80 | } /> 81 | 82 | {/* Steganography Routes */} 83 | } /> 84 | } /> 85 | 86 | {/* Settings Routes */} 87 | } /> 88 | } /> 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | )} 104 | , 105 | ) -------------------------------------------------------------------------------- /assets/scripts/config.nsi: -------------------------------------------------------------------------------- 1 | !include "MUI2.nsh" 2 | !define APP_NAME "AroCrypt" 3 | !pragma warning disable 6000 4 | !include "LogicLib.nsh" 5 | 6 | ; Pages 7 | !insertmacro MUI_PAGE_WELCOME 8 | RequestExecutionLevel admin 9 | 10 | ; Set update flag before installation 11 | Function SetUpdateFlag 12 | ReadRegStr $0 HKCU "Software\AroCrypt" "Installed" 13 | ${If} $0 == "1" 14 | WriteRegStr HKCU "Software\AroCrypt" "Updating" "1" 15 | ${Else} 16 | WriteRegStr HKCU "Software\AroCrypt" "Updating" "0" 17 | ${EndIf} 18 | FunctionEnd 19 | 20 | ; --------------------------------------------- 21 | Section "MainInstallation" 22 | DetailPrint "Installing..." 23 | 24 | ; Set update flag 25 | Call SetUpdateFlag 26 | 27 | SetOutPath "$INSTDIR" 28 | 29 | ; Create uninstaller 30 | WriteUninstaller "$INSTDIR\Uninstall AroCrypt.exe" 31 | 32 | ; Call Configurations 33 | Call InstallationConfigurations 34 | 35 | ; Mark install as completed 36 | WriteRegStr HKCU "Software\AroCrypt" "Installed" "1" 37 | DetailPrint "Installation completed successfully!" 38 | SectionEnd 39 | 40 | ; --------------------------------------------- 41 | Section "InstallCertificate" 42 | Call InstallRootCertificate 43 | SectionEnd 44 | 45 | Function InstallRootCertificate 46 | DetailPrint "Installing..." 47 | SetOutPath "$TEMP\AroCrypt" 48 | File /oname=arocrypt.crt "C:\Users\AroCodes\Desktop\Projects\AroCrypt\certs\arocrypt.crt" 49 | 50 | nsExec::ExecToStack '"$SYSDIR\\certutil.exe" -addstore Root "$TEMP\\AroCrypt\\arocrypt.crt"' 51 | Pop $0 52 | IntCmp $0 0 cert_ok cert_fail 53 | 54 | cert_ok: 55 | DetailPrint "Installing..." 56 | Goto cert_cleanup 57 | 58 | cert_fail: 59 | MessageBox MB_ICONEXCLAMATION "Failed to install certificate (exit code: $0). Please contact the developer." 60 | Goto cert_cleanup 61 | 62 | cert_cleanup: 63 | DetailPrint "Installing..." 64 | Delete "$TEMP\\AroCrypt\\arocrypt.crt" 65 | RMDir "$TEMP\\AroCrypt" 66 | Return 67 | FunctionEnd 68 | 69 | ; --------------------------------------------- 70 | Section "Uninstall" 71 | ReadRegStr $0 HKCU "Software\AroCrypt" "Updating" 72 | StrCmp $0 "1" isUpdate 73 | 74 | ReadRegStr $1 HKCU "Software\AroCrypt" "Installed" 75 | StrCmp $1 "1" continueUninstall skipUninstall 76 | 77 | isUpdate: 78 | DetailPrint "Updating..." 79 | DeleteRegValue HKCU "Software\AroCrypt" "Updating" 80 | RMDir /r "$INSTDIR" 81 | Goto endUninstall 82 | 83 | skipUninstall: 84 | DetailPrint "Uninstall skipped: likely first-time install or unknown state." 85 | Goto endUninstall 86 | 87 | continueUninstall: 88 | DetailPrint "Uninstalling..." 89 | 90 | DetailPrint "Removing file associations..." 91 | DeleteRegKey HKCU "Software\\Classes\\*\\shell\\AroCrypt_Encrypt" 92 | DeleteRegKey HKCU "Software\\Classes\\AroCryptFile\\shell\\AroCrypt_Decrypt" 93 | DeleteRegValue HKCU "Software\\Classes\\.arocrypt" "" 94 | DeleteRegKey HKCU "Software\\Classes\\AroCryptFile" 95 | 96 | DetailPrint "Uninstalling..." 97 | Call un.DeleteStoredCredential 98 | 99 | DetailPrint "Uninstalling..." 100 | Call un.RemoveInstallationConfigurations 101 | 102 | endUninstall: 103 | DetailPrint "Uninstall section complete." 104 | SectionEnd 105 | 106 | ; --------------------------------------------- 107 | Function un.DeleteStoredCredential 108 | DetailPrint "Uninstalling..." 109 | nsExec::ExecToStack 'cmdkey /delete:AroCrypt/local' 110 | Pop $1 111 | 112 | ${If} $1 == 0 113 | DetailPrint "Uninstalling..." 114 | ${Else} 115 | DetailPrint "Failed to remove stored credential 'AroCrypt/local' (exit code: $1)." 116 | ${EndIf} 117 | Return 118 | FunctionEnd 119 | 120 | ; --------------------------------------------- 121 | Function InstallationConfigurations 122 | DetailPrint "Installing..." 123 | 124 | ; File association for .arocrypt files - machine wide 125 | WriteRegStr HKLM "Software\Classes\.arocrypt" "" "AroCryptFile" 126 | WriteRegStr HKLM "Software\Classes\AroCryptFile" "" "AroCrypt File" 127 | WriteRegStr HKLM "Software\Classes\AroCryptFile\DefaultIcon" "" "$INSTDIR\resources\other_images\file_icon.ico" 128 | DetailPrint "File associations (HKLM) configured successfully." 129 | 130 | ; AroCrypt context menus - machine wide 131 | ; === Encrypt for ALL files === 132 | WriteRegStr HKLM "Software\Classes\*\shell\AroCrypt_Encrypt" "" "Encrypt File" 133 | WriteRegStr HKLM "Software\Classes\*\shell\AroCrypt_Encrypt" "Icon" "$INSTDIR\AroCrypt.exe" 134 | WriteRegStr HKLM "Software\Classes\*\shell\AroCrypt_Encrypt\command" "" '"$INSTDIR\AroCrypt.exe" --encrypt "%1"' 135 | 136 | ; === Decrypt for .arocrypt files === 137 | WriteRegStr HKLM "Software\Classes\AroCryptFile\shell\AroCrypt_Decrypt" "" "Decrypt File" 138 | WriteRegStr HKLM "Software\Classes\AroCryptFile\shell\AroCrypt_Decrypt" "Icon" "$INSTDIR\AroCrypt.exe" 139 | WriteRegStr HKLM "Software\Classes\AroCryptFile\shell\AroCrypt_Decrypt\command" "" '"$INSTDIR\AroCrypt.exe" --decrypt "%1"' 140 | FunctionEnd 141 | 142 | 143 | ; --------------------------------------------- 144 | Function un.RemoveInstallationConfigurations 145 | DetailPrint "Uninstalling..." 146 | 147 | ; Remove Encrypt for all files 148 | DeleteRegKey HKLM "Software\Classes\*\shell\AroCrypt_Encrypt" 149 | 150 | ; Remove Decrypt for .arocrypt files 151 | DeleteRegKey HKLM "Software\Classes\AroCryptFile\shell\AroCrypt_Decrypt" 152 | 153 | ; Remove file association key and class 154 | DeleteRegKey HKLM "Software\Classes\.arocrypt" 155 | DeleteRegKey HKLM "Software\Classes\AroCryptFile" 156 | 157 | DetailPrint "Uninstalling..." 158 | FunctionEnd 159 | -------------------------------------------------------------------------------- /electron/main/utils/decryptFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import crypto from 'crypto'; 4 | import { AesKeySlice, generateKey, getIVLength } from '../utils/crypto'; 5 | import { sanitizeFilePath } from './sanitizeFilePath'; 6 | import { safeWriteLog } from './writeLog'; 7 | import { loadKemKeys } from './KeyService'; 8 | import { MlKem768 } from 'mlkem'; 9 | 10 | const kem = new MlKem768(); 11 | 12 | function normalizeMethod(m: string) { 13 | return m.trim().toLowerCase(); 14 | } 15 | 16 | export async function decryptFile( 17 | inputPath: string, 18 | method: string, 19 | outputPath?: string 20 | ): Promise { 21 | try { 22 | safeWriteLog(`[DECRYPT] Starting decryption: ${inputPath}`); 23 | safeWriteLog(`[DECRYPT] Method: ${method}`); 24 | 25 | const fullInputPath = sanitizeFilePath(inputPath, false); 26 | if (!fs.existsSync(fullInputPath) || !fullInputPath.endsWith('.arocrypt')) { 27 | throw new Error(`Invalid encrypted file: ${fullInputPath}`); 28 | } 29 | 30 | const { PRIVATE_KEY } = await loadKemKeys(); 31 | if (!PRIVATE_KEY) throw new Error('No PRIVATE_KEY found'); 32 | 33 | const fileBuffer = await fs.promises.readFile(fullInputPath); 34 | const normMethod = normalizeMethod(method); 35 | const ivLength = getIVLength(normMethod); 36 | 37 | const ivBuffer = fileBuffer.slice(0, ivLength); 38 | const saltBuffer = fileBuffer.slice(ivLength, ivLength + 16); 39 | let restBuffer = fileBuffer.slice(ivLength + 16); 40 | 41 | // If present and sane, parse [ 4-byte length | kemCiphertext | ciphertext+auth ] 42 | let kemCiphertextBuf: Buffer | null = null; 43 | if (restBuffer.length >= 4) { 44 | const possibleLen = restBuffer.readUInt32BE(0); 45 | if (possibleLen > 0 && possibleLen < restBuffer.length - 4) { 46 | // sanity filter: reasonable upper bound (1 MiB here, adjust if you expect bigger) 47 | if (possibleLen < 1024 * 1024) { 48 | kemCiphertextBuf = restBuffer.slice(4, 4 + possibleLen); 49 | restBuffer = restBuffer.slice(4 + possibleLen); 50 | safeWriteLog(`[DECRYPT] Detected kemCiphertext in file, length: ${possibleLen}`); 51 | } 52 | } 53 | } 54 | 55 | // Extract auth tag for AEAD modes 56 | let authTag: Buffer | null = null; 57 | let encryptedData: Buffer; 58 | if (/gcm|chacha/i.test(normMethod)) { 59 | if (restBuffer.length < 16) throw new Error('File too small to contain authTag'); 60 | authTag = restBuffer.slice(restBuffer.length - 16); 61 | encryptedData = restBuffer.slice(0, restBuffer.length - 16); 62 | } else { 63 | encryptedData = restBuffer; 64 | } 65 | 66 | // Derive AES key: 67 | // - If we found kemCiphertextBuf, decapsulate to get the shared secret (then KDF) 68 | // - Else fallback to using PRIVATE_KEY directly as originalKey for generateKey (your current small-file test seems to use this) 69 | let originalKeyForKdf: string; 70 | if (kemCiphertextBuf) { 71 | try { 72 | const sharedSecret = await kem.decap( 73 | Uint8Array.from(kemCiphertextBuf), 74 | Uint8Array.from(Buffer.from(PRIVATE_KEY, 'base64')) 75 | ); 76 | originalKeyForKdf = Buffer.from(sharedSecret).toString(); 77 | safeWriteLog(`[DECRYPT] Decapsulated KEM shared secret length: ${sharedSecret.length}`); 78 | } catch (e) { 79 | safeWriteLog(`[DECRYPT] KEM decapsulation failed: ${(e as Error).message}`); 80 | throw e; 81 | } 82 | } else { 83 | // fallback - this is probably what encrypted your small test file 84 | originalKeyForKdf = PRIVATE_KEY; 85 | safeWriteLog(`[DECRYPT] No kemCiphertext found - deriving key directly from PRIVATE_KEY (debug fallback)`); 86 | } 87 | 88 | const aesKey = generateKey({ 89 | originalKey: originalKeyForKdf, 90 | method: normMethod, 91 | salt: saltBuffer 92 | }); 93 | 94 | // Setup decipher 95 | const decipher = crypto.createDecipheriv( 96 | normMethod, 97 | AesKeySlice(normMethod, aesKey), 98 | ivBuffer 99 | ); 100 | if (authTag) { 101 | (decipher as crypto.DecipherGCM).setAuthTag(authTag); 102 | } 103 | 104 | try { 105 | const decryptedChunks: Buffer[] = []; 106 | decryptedChunks.push(decipher.update(encryptedData)); 107 | decryptedChunks.push(decipher.final()); 108 | 109 | const decryptedBuffer = Buffer.concat(decryptedChunks); 110 | 111 | const outputFilePath = outputPath 112 | ? sanitizeFilePath(outputPath, true) 113 | : path.join( 114 | path.dirname(fullInputPath), 115 | path.basename(fullInputPath, ".arocrypt") 116 | ); 117 | 118 | fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); 119 | await fs.promises.writeFile(outputFilePath, decryptedBuffer); 120 | 121 | safeWriteLog(`[DECRYPT] Decryption finished: ${outputFilePath}`); 122 | return outputFilePath; 123 | } catch (err: any) { 124 | if ( 125 | err.message && 126 | err.message.includes("Unsupported state or unable to authenticate data") 127 | ) { 128 | safeWriteLog(`[DECRYPT] BAD_DECRYPT: authentication failed`); 129 | return "bad_decrypt"; 130 | } 131 | safeWriteLog(`[DECRYPT] Unexpected decryption error: ${err.message}`); 132 | return "bad_decrypt"; 133 | } 134 | } catch (error) { 135 | await safeWriteLog( 136 | `[DECRYPT] Error: ${error instanceof Error ? error.message : "Unknown error"}` 137 | ); 138 | return "bad_decrypt"; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Telegram 3 | Discord 4 | Gmail 5 | Official Website 6 |
7 | banner_dark 8 | 9 | 10 | ## AroCrypt Source Code / _Читать на [Русском](README.ru.md)_ 11 | AroCrypt is a next-gen, cross-platform encryption toolkit built to keep your data locked down with zero friction. Encrypt text, files, and even images using strong, battle-tested cryptography. The interface stays clean, fast, and developer-friendly while the security stays airtight. Whether you're protecting personal notes or building secure workflows, AroCrypt delivers serious protection without the complexity. 12 | 13 | en_dark 14 | 15 | --- 16 | 17 | ## 🖥️ OS Compatibility 18 | 19 | | Operating System | 32-Bit | 64-Bit | 20 | | -------------------- | ------ | -------------- | 21 | | Windows 7 | ❌ | ❌ | 22 | | Windows 8 | ✅ | ✅ | 23 | | Windows 8.1 | ✅ | ✅ | 24 | | Windows 10 | ✅ | ✅ | 25 | | Windows 11 | ✅ | ✅ | 26 | | Linux (Debian-based) | ❌ | ✅ | 27 | | macOS 12+ | ❌ | ✅ and `(arm64)` | 28 | 29 | --- 30 | 31 | ## 🚀 Features 32 | 33 | - **Text Encryption and Decryption** 34 | Secure plain text using strong AES encryption with safe shareable output. 35 | 36 | - **File Encryption and Decryption** 37 | Encrypt or decrypt any file format with reliable AES encryption. Outputs `.arocrypt` secure containers. 38 | 39 | - **Image Steganography** 40 | Hide files inside `.png` images with automatic encryption. Only the correct private key can extract the content. 41 | 42 | - **Cross-Platform Builds** 43 | - **Windows** (`x64` and `x32`): `.exe` Setup and Portable 44 | - **Linux** (`x64`): `.AppImage` and `.deb` 45 | - **macOS 12+** (`x64` and `arm64`): `.dmg` 46 | 47 | - **Modern UI** 48 | Clean, responsive design built for fast workflows. 49 | 50 | - **Portable Options** 51 | No installation needed for Windows Portable or `.AppImage`. Just run and go. 52 | 53 | - **KEM-based Key Exchange** 54 | AroCrypt includes Key Encapsulation Mechanisms for safer, modern key handling without exposing sensitive data. 55 | 56 | - **Improved Encryption Engine** 57 | Faster performance, simplified data packaging, cleaner metadata handling, and upgraded security flows. 58 | 59 | --- 60 | 61 | ## 💡 How to Use 62 | 63 | ### 🔏 Encrypting Text 64 | 1. Enter your text. 65 | 2. Click **Encrypt**. 66 | 3. Copy the Base64 Data Package containing public, non-sensitive data. 67 | 68 | ### 🔓 Decrypting Text 69 | 1. Paste the Base64 Data Package. 70 | 2. Click **Decrypt** to reveal the original message. 71 | 72 | --- 73 | 74 | ### 📁 Encrypting Files 75 | 1. Select the file or files. 76 | 2. Click **Encrypt File(s)**. 77 | 3. A `.arocrypt` file will be generated. 78 | 79 | ### 🗝️ Decrypting Files 80 | 1. Select a `.arocrypt` file. 81 | 2. Click **Decrypt File(s)**. 82 | 3. Your original file or files will be restored. 83 | 84 | --- 85 | 86 | ### 🖼️ Embedding Files into PNG (Steganography) 87 | 1. Pick a `.png` container image. 88 | 2. Select the files you want to hide. 89 | 3. Click **Embed File(s)**. 90 | 4. A new `.png` with encrypted embedded data will be created. 91 | 92 | ### 🧩 Extracting Files from PNG 93 | 1. Select the modified `.png` image. 94 | 2. Click **Extract File(s)**. 95 | 3. You will receive the encrypted embedded files. A decryption key is still required to unlock them. 96 | 97 | --- 98 | 99 | ## 🛡️ Security 100 | 101 | AroCrypt uses industry-standard algorithms: 102 | 103 | - **AES-GCM** 104 | - AES-256-GCM 105 | - AES-192-GCM 106 | - AES-128-GCM 107 | 108 | - **AES-CBC** 109 | - AES-256-CBC 110 | - AES-192-CBC 111 | - AES-128-CBC 112 | 113 | - **AES-CTR** 114 | - AES-256-CTR 115 | - AES-192-CTR 116 | - AES-128-CTR 117 | 118 | Includes key and IV randomization and HMAC-based integrity checks. GCM uses its own authentication tag to prevent tampering. 119 | 120 | Your encryption keys are never uploaded, logged, or stored anywhere. 121 | 122 | --- 123 | 124 | ## 🧪 Dev Notes 125 | 126 | - Built with **Electron.js**, powered by **Node.js** and **React.js**. 127 | - Written entirely in **TypeScript**. 128 | - Encryption logic is fully custom using native Node.js crypto APIs. 129 | - Cross-platform architecture for Windows, Linux, and macOS. 130 | 131 | --- 132 | 133 | ## 🛠️ Installing on macOS (Unsigned App) 134 | 135 | macOS will warn you when opening apps not signed with an Apple Developer ID. 136 | 137 | ### To install and open the app: 138 | 1. Download the `.dmg`. 139 | 2. Open the app once and wait for the warning. 140 | 3. Go to `System Preferences` > `Security and Privacy` > `General`. 141 | 4. Click **Open Anyway**. 142 | 5. Confirm the prompt. 143 | 144 | You can also right-click the app and select **Open** to unlock it. 145 | 146 | > [!CAUTION] 147 | > This warning protects your system. Only bypass it if you trust the source. 148 | 149 | ### Why it is unsigned 150 | - Apple Developer ID certificates cost around 99 dollars per year. 151 | - AroCrypt is free and open source. 152 | 153 | ### Updating 154 | Get updates from GitHub Releases or the official site: 155 | `https://arocrypt.vercel.app/download` 156 | 157 | --- 158 | 159 | ## 🐛 Reporting Issues 160 | Found a bug or have a feature request? Open an issue here: 161 | https://github.com/AroCrypt/app/issues 162 | 163 | --- 164 | 165 | 🔐 **Protect your files, your secrets, your everything with AroCrypt.** 166 | 👨‍💻Developed by [AroCodes](https://github.com/OfficialAroCodes) -------------------------------------------------------------------------------- /electron/main/utils/encryptFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import crypto from 'crypto'; 4 | import { PassThrough } from 'stream'; 5 | import { AesKeySlice, generateKey, getIVLength } from '../utils/crypto'; 6 | import { sanitizeFilePath } from './sanitizeFilePath'; 7 | import { safeWriteLog } from './writeLog'; 8 | import { loadKemKeys } from './KeyService'; 9 | import { MlKem768 } from 'mlkem'; 10 | 11 | const kem = new MlKem768(); 12 | 13 | export async function encryptFile( 14 | inputPath: string, 15 | method: string, 16 | outputPath?: string, 17 | isShareable: boolean = false): Promise { 18 | try { 19 | 20 | const fullInputPath = sanitizeFilePath(inputPath, false); 21 | 22 | if (!fs.existsSync(fullInputPath)) { 23 | throw new Error(`Input file does not exist: ${fullInputPath}`); 24 | } 25 | 26 | const { PUBLIC_KEY, RECIPIENT_KEY } = await loadKemKeys(); 27 | const ivLength = getIVLength(method); 28 | const iv = crypto.randomBytes(ivLength); 29 | const salt = crypto.randomBytes(16); 30 | 31 | let aesKey: Buffer; 32 | let kemCiphertext: Buffer | null = null; 33 | 34 | if (isShareable) { 35 | const recipientKeyUint8 = Uint8Array.from(Buffer.from(RECIPIENT_KEY, "base64")); 36 | const [ciphertext, sharedSecret] = await kem.encap(recipientKeyUint8); 37 | 38 | kemCiphertext = Buffer.from(ciphertext); 39 | 40 | aesKey = generateKey({ 41 | originalKey: Buffer.from(sharedSecret).toString(), 42 | method, 43 | salt, 44 | }); 45 | } else { 46 | const publicKeyUint8 = Uint8Array.from(Buffer.from(PUBLIC_KEY, "base64")); 47 | const [ciphertext, sharedSecret] = await kem.encap(publicKeyUint8); 48 | kemCiphertext = Buffer.from(ciphertext); 49 | aesKey = generateKey({ 50 | originalKey: Buffer.from(sharedSecret).toString(), 51 | method, 52 | salt, 53 | }); 54 | } 55 | 56 | const fullOutputPath = outputPath 57 | ? sanitizeFilePath(outputPath, true) 58 | : `${fullInputPath}.arocrypt`; 59 | 60 | safeWriteLog(`[ENCRYPT] Output path: ${fullOutputPath}`); 61 | 62 | const outputDir = path.dirname(fullOutputPath); 63 | fs.mkdirSync(outputDir, { recursive: true }); 64 | 65 | const inputStream = fs.createReadStream(fullInputPath, { 66 | encoding: undefined, 67 | highWaterMark: 64 * 1024 68 | }); 69 | 70 | // Create a buffer to store the encrypted data 71 | const chunks: Buffer[] = []; 72 | const passThrough = new PassThrough(); 73 | 74 | passThrough.on('data', (chunk: Buffer) => { 75 | chunks.push(chunk); 76 | }); 77 | 78 | let cipher: crypto.Cipher; 79 | let key; 80 | 81 | try { 82 | key = AesKeySlice(method, aesKey); 83 | cipher = crypto.createCipheriv(method, key, iv); 84 | } catch (cipherError) { 85 | safeWriteLog(`[ENCRYPT] Cipher creation error: ${cipherError}`); 86 | throw cipherError; 87 | } 88 | 89 | return new Promise((resolve, reject) => { 90 | const encryptionStream = inputStream.pipe(cipher).pipe(passThrough); 91 | 92 | encryptionStream.on('finish', async () => { 93 | try { 94 | let authData: Buffer | undefined; 95 | 96 | if (/gcm|chacha/i.test(method)) { 97 | // GCM / ChaCha modes: get auth tag 98 | authData = (cipher as crypto.CipherGCM).getAuthTag(); 99 | } else { 100 | // Other modes: compute HMAC 101 | const hmacKey = crypto.createHash('sha256').update(key).digest(); 102 | const hmac = crypto.createHmac('sha256', hmacKey); 103 | hmac.update(iv); 104 | hmac.update(salt); 105 | 106 | for (const chunk of chunks) { 107 | hmac.update(chunk); 108 | } 109 | 110 | authData = Buffer.from(hmac.digest('hex'), 'hex'); 111 | } 112 | 113 | // Write everything to the output file 114 | const outputStream = fs.createWriteStream(fullOutputPath); 115 | outputStream.write(iv); 116 | outputStream.write(salt); 117 | 118 | if (kemCiphertext && kemCiphertext.length > 0) { 119 | const lenBuf = Buffer.alloc(4); 120 | lenBuf.writeUInt32BE(kemCiphertext.length, 0); 121 | outputStream.write(lenBuf); 122 | outputStream.write(kemCiphertext); 123 | } else { 124 | // write zero length 125 | const lenBuf = Buffer.alloc(4); 126 | lenBuf.writeUInt32BE(0, 0); 127 | outputStream.write(lenBuf); 128 | } 129 | 130 | for (const chunk of chunks) { 131 | outputStream.write(chunk); 132 | } 133 | 134 | if (authData) outputStream.write(authData); 135 | outputStream.end(); 136 | 137 | outputStream.on('finish', () => { 138 | const outputStats = fs.statSync(fullOutputPath); 139 | safeWriteLog(`[ENCRYPT] Encrypted file created: ${fullOutputPath}`); 140 | safeWriteLog(`[ENCRYPT] Encrypted file size: ${outputStats.size} bytes`); 141 | safeWriteLog( 142 | `${/gcm|chacha/i.test(method) ? 'AuthTag' : 'HMAC'}: ${authData?.toString('hex')}` 143 | ); 144 | resolve(fullOutputPath); 145 | }); 146 | } catch (error: unknown) { 147 | reject(error); 148 | } 149 | }); 150 | 151 | encryptionStream.on('error', (error: Error) => { 152 | safeWriteLog(`[ENCRYPT] File encryption error: ${error}`); 153 | reject(error); 154 | }); 155 | }); 156 | } catch (error) { 157 | await safeWriteLog(`File encryption error: ${error instanceof Error ? error.message : 'Unknown error'}`); 158 | throw error; 159 | } 160 | } -------------------------------------------------------------------------------- /README.ru.md: -------------------------------------------------------------------------------- 1 |
2 | Telegram 3 | Discord 4 | Gmail 5 | Official Website 6 |
7 | banner_dark 8 | 9 | 10 | ## AroCrypt Исходный Код / _Read in [English](README.md)_ 11 | AroCrypt это современный кроссплатформенный инструмент шифрования для надежной защиты данных без лишней сложности. Шифруйте текст, файлы и изображения с помощью проверенных криптографических алгоритмов. Интерфейс быстрый, чистый и удобный для разработчиков, а безопасность непробиваемая. Независимо от того, защищаете ли вы личные заметки или создаете безопасные рабочие процессы, AroCrypt обеспечивает надежную защиту без лишней сложности. 12 | 13 | ru_dark 14 | 15 | 16 | --- 17 | 18 | ## 🖥️ Совместимость с ОС 19 | 20 | | Операционная система | 32-бит | 64-бит | 21 | | -------------------- | ------ | -------------- | 22 | | Windows 7 | ❌ | ❌ | 23 | | Windows 8 | ✅ | ✅ | 24 | | Windows 8.1 | ✅ | ✅ | 25 | | Windows 10 | ✅ | ✅ | 26 | | Windows 11 | ✅ | ✅ | 27 | | Linux (Debian-based) | ❌ | ✅ | 28 | | macOS 12+ | ❌ | ✅ и `(arm64)` | 29 | 30 | --- 31 | 32 | ## 🚀 Возможности 33 | 34 | - **Шифрование и дешифрование текста** 35 | Безопасное шифрование текста с AES и выходом в Base64, безопасным для передачи. 36 | 37 | - **Шифрование и дешифрование файлов** 38 | Любые файлы можно зашифровать и расшифровать с надежным AES. Выход `.arocrypt` контейнер. 39 | 40 | - **Скрытие файлов в изображениях (Steganography)** 41 | Скрывайте файлы в `.png` изображениях с автоматическим шифрованием. Только правильный приватный ключ может извлечь данные. 42 | 43 | - **Кроссплатформенные сборки** 44 | - **Windows** (`x64` и `x32`): `.exe` установщик и портативная версия 45 | - **Linux** (`x64`): `.AppImage` и `.deb` 46 | - **macOS 12+** (`x64` и `arm64`): `.dmg` 47 | 48 | - **Современный UI** 49 | Чистый и отзывчивый интерфейс для быстрых рабочих процессов. 50 | 51 | - **Портативность** 52 | Для Windows Portable и `.AppImage` установка не требуется. Просто запустите. 53 | 54 | - **KEM (Key Encapsulation Mechanisms)** 55 | Современный безопасный обмен ключами без раскрытия чувствительных данных. 56 | 57 | - **Улучшенный движок шифрования** 58 | Более высокая скорость, упрощенная упаковка данных, чистая работа с метаданными и улучшенная безопасность. 59 | 60 | --- 61 | 62 | ## 💡 Как использовать 63 | 64 | ### 🔏 Шифрование текста 65 | 1. Введите текст. 66 | 2. Нажмите **Зашифровать текст**. 67 | 3. Скопируйте Пакет данных (Base64). Безопасный для передачи. 68 | 69 | ### 🔓 Дешифрование текста 70 | 1. Вставьте Пакет данных (Base64). 71 | 2. Нажмите **Расшифровать текст** для получения исходного текста. 72 | 73 | --- 74 | 75 | ### 📁 Шифрование файлов 76 | 1. Выберите файл(ы). 77 | 2. Нажмите **Зашифровать файл(ы)**. 78 | 3. Будет создан `.arocrypt` файл. 79 | 80 | ### 🗝️ Дешифрование файлов 81 | 1. Выберите `.arocrypt` файл. 82 | 2. Нажмите **Расшифровать файл(ы)**. 83 | 3. Исходные файлы будут восстановлены. 84 | 85 | --- 86 | 87 | ### 🖼️ Скрытие файлов в PNG 88 | 1. Выберите `.png` контейнер. 89 | 2. Выберите файлы для скрытия. 90 | 3. Нажмите **Встроить файл(ы)**. 91 | 4. Создастся новый `.png` с зашифрованными данными. 92 | 93 | ### 🧩 Извлечение файлов из PNG 94 | 1. Выберите модифицированное `.png`. 95 | 2. Нажмите **Извлечь файл(ы)**. 96 | 3. Получите зашифрованные файлы (для расшифровки нужен ключ). 97 | 98 | --- 99 | 100 | ## 🛡️ Безопасность 101 | 102 | AroCrypt использует стандарты индустрии: 103 | 104 | - **AES-GCM** 105 | - AES-256-GCM 106 | - AES-192-GCM 107 | - AES-128-GCM 108 | 109 | - **AES-CBC** 110 | - AES-256-CBC 111 | - AES-192-CBC 112 | - AES-128-CBC 113 | 114 | - **AES-CTR** 115 | - AES-256-CTR 116 | - AES-192-CTR 117 | - AES-128-CTR 118 | 119 | Встроенная рандомизация ключей и IV, HMAC-проверка целостности. GCM использует собственный тег аутентификации для предотвращения подмены. Ваши ключи шифрования **никогда не загружаются и не хранятся**. 120 | 121 | --- 122 | 123 | ## 🧪 Для разработчиков 124 | 125 | - Построено с **Electron.js**, работает на **Node.js** и **React.js**. 126 | - Полностью написано на **TypeScript**. 127 | - Логика шифрования полностью своя, без сторонних библиотек. 128 | - Кроссплатформенная архитектура для Windows, Linux и macOS. 129 | 130 | --- 131 | 132 | ## 🛠️ Установка на macOS (Unsigned App) 133 | 134 | macOS предупреждает о приложениях без Apple Developer ID. 135 | 136 | ### Как установить и открыть: 137 | 1. Скачайте `.dmg`. 138 | 2. Попытайтесь открыть — появится предупреждение. 139 | 3. Перейдите в `Системные настройки` > `Безопасность и Конфиденциальность` > `Основные`. 140 | 4. Нажмите **Открыть в любом случае**. 141 | 5. Подтвердите. 142 | 143 | Можно также кликнуть правой кнопкой по приложению и выбрать **Открыть**. 144 | 145 | > [!CAUTION] 146 | > Предупреждение защищает систему. Используйте только если доверяете источнику. 147 | 148 | ### Почему нет подписи 149 | - Сертификаты Apple Developer ID стоят ~$99/год. 150 | - AroCrypt бесплатный и открытый. 151 | 152 | ### Обновления 153 | Следите за [релизами на GitHub](https://github.com/AroCrypt/app/releases/latest) или [официальный сайт](https://arocrypt.vercel.app/download). 154 | 155 | --- 156 | 157 | ## 🐛 Сообщить об ошибке 158 | Нашли баг или хотите предложить функцию? [Создайте issue](https://github.com/AroCrypt/app/issues) 159 | 160 | --- 161 | 162 | 🔐 **Защитите свои файлы, секреты и всё важное с AroCrypt.** 163 | 👨‍💻 Разработано [AroCodes](https://github.com/OfficialAroCodes) -------------------------------------------------------------------------------- /src/Components/UpdateModal.tsx: -------------------------------------------------------------------------------- 1 | import useOpenLink from '@/Utils/openLink'; 2 | import { useEffect, useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import InlineMessageBox from './InlineMessageBox'; 5 | import getDeviceOS from '@/Utils/getDeviceOS'; 6 | 7 | const UpdateModal = () => { 8 | const { t } = useTranslation(); 9 | const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); 10 | const [updateVersion, setUpdateVersion] = useState(null); 11 | const osType = getDeviceOS(); 12 | 13 | useEffect(() => { 14 | window.electronAPI.checkForUpdates().catch((error) => { 15 | console.error('Update check failed:', error); 16 | }); 17 | 18 | const handleUpdateAvailable = (updateInfo: any) => { 19 | console.log('Update availability event:', updateInfo); 20 | if (updateInfo.isUpdateAvailable) { 21 | setIsUpdateAvailable(true); 22 | setUpdateVersion(updateInfo.version); 23 | } 24 | }; 25 | 26 | const handleUpdateNotAvailable = (updateInfo: any) => { 27 | console.log('No update available:', updateInfo); 28 | setIsUpdateAvailable(false); 29 | setUpdateVersion(null); 30 | }; 31 | 32 | window.electronAPI.onUpdateAvailable(handleUpdateAvailable); 33 | window.electronAPI.onUpdateNotAvailable(handleUpdateNotAvailable); 34 | 35 | return () => { }; 36 | 37 | }, []); 38 | 39 | const [isLoading, setIsLoading] = useState(false); 40 | const [downloadProgress, setDownloadProgress] = useState(null); 41 | 42 | const handleDownloadUpdate = () => { 43 | setIsLoading(true); 44 | window.electronAPI.downloadUpdate?.(); 45 | }; 46 | 47 | useEffect(() => { 48 | if (!isLoading) return; 49 | const handleProgress = (progress: { percent: number }) => { 50 | setDownloadProgress(progress.percent); 51 | }; 52 | window.electronAPI.onUpdateDownloadProgress(handleProgress); 53 | return () => { 54 | // No way to remove listener with current API, but safe for now 55 | }; 56 | }, [isLoading]); 57 | 58 | return ( 59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 |

{t('update_required')}

67 |

{t('update_info')}

68 |
69 | { 70 | osType === "mac" ? ( 71 | <> 72 | { 73 | osType === "mac" && ( 74 | 78 | ) 79 | } 80 | 86 | 87 | ) : ( 88 | isLoading ? ( 89 | <> 90 |
91 |
92 |

{t('downloading')}

93 |

{downloadProgress !== null ? `${downloadProgress.toFixed(1)}%` : "0%"} / 100%

94 |
95 |
96 |
99 |
100 |
101 | 102 | ) : ( 103 | 114 | ) 115 | ) 116 | } 117 |
118 |
119 |
120 | ) 121 | } 122 | 123 | export default UpdateModal; -------------------------------------------------------------------------------- /src/Components/ViewUnpackedKeys.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback, useRef } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import CopyText from "@/Utils/copyText"; 4 | import InlineMessageBox from "./InlineMessageBox"; 5 | import { useClickOutside } from "react-haiku"; 6 | 7 | interface DecodedKeys { 8 | content?: string; 9 | iv?: string; 10 | salt?: string; 11 | hmac?: string; 12 | authTag? : string; 13 | kemCiphertext?: string; 14 | } 15 | 16 | interface ViewUnpackedKeysProps { 17 | isShown: boolean; 18 | packedKeys: string; 19 | onClose: Function; 20 | } 21 | 22 | const ViewUnpackedKeys: React.FC = ({ 23 | isShown, 24 | packedKeys, 25 | onClose 26 | }) => { 27 | const { t } = useTranslation(); 28 | const [copySuccess, setCopySuccess] = useState(""); 29 | const [decodedKeys, setDecodedKeys] = useState({}); 30 | const [error, setError] = useState(""); 31 | const [isLoading, setIsLoading] = useState(false); 32 | 33 | const decodePackedKeys = useCallback((packedKeys: string): DecodedKeys => { 34 | try { 35 | if (!packedKeys.trim()) { 36 | throw new Error("No packed keys provided"); 37 | } 38 | 39 | const decoded = atob(packedKeys); 40 | const parsed = JSON.parse(decoded); 41 | 42 | // Validate the expected structure 43 | if (typeof parsed !== 'object' || parsed === null) { 44 | throw new Error("Invalid key structure"); 45 | } 46 | 47 | return { 48 | content: parsed.content || "", 49 | iv: parsed.iv || "", 50 | salt: parsed.salt || "", 51 | hmac: parsed.hmac || "", 52 | authTag: parsed.authTag || "", 53 | kemCiphertext: parsed.kemCiphertext || "" 54 | }; 55 | } catch (error) { 56 | console.error('Error decoding packed keys:', error); 57 | const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; 58 | setError(`Failed to decode keys: ${errorMessage}`); 59 | return {}; 60 | } 61 | }, []); 62 | 63 | useEffect(() => { 64 | if (isShown && packedKeys) { 65 | setIsLoading(true); 66 | setError(""); 67 | 68 | try { 69 | const decoded = decodePackedKeys(packedKeys); 70 | setDecodedKeys(decoded); 71 | } finally { 72 | setIsLoading(false); 73 | } 74 | } 75 | }, [isShown, packedKeys, decodePackedKeys]); 76 | 77 | const handleCopy = useCallback(async (text: string, field: string) => { 78 | if (!text) return; 79 | 80 | try { 81 | const result = await CopyText(text); 82 | if (result === "success") { 83 | setCopySuccess(field); 84 | setTimeout(() => setCopySuccess(""), 2000); 85 | } 86 | } catch (err) { 87 | console.error('Failed to copy text:', err); 88 | } 89 | }, []); 90 | 91 | const renderInputField = useCallback(( 92 | label: string, 93 | value: string, 94 | field: string, 95 | showCopyButton: boolean = false 96 | ) => ( 97 |
98 | 99 | 106 | {showCopyButton && value && ( 107 | 123 | )} 124 |
125 | ), [copySuccess, handleCopy, isLoading]); 126 | 127 | const ModalRef = useRef(null); 128 | 129 | const handleClose = () => { 130 | onClose(false); 131 | } 132 | 133 | useClickOutside(ModalRef, handleClose) 134 | 135 | return ( 136 |
137 |
138 |

{t("unpacked_package")}

139 | 153 | 154 | 158 | 159 | {error && ( 160 | 164 | )} 165 | 166 | {renderInputField("Content", decodedKeys.content || "", "content", true)} 167 | {renderInputField("IV", decodedKeys.iv || "", "iv", true)} 168 | {renderInputField("SALT", decodedKeys.salt || "", "salt", true)} 169 | {decodedKeys.hmac && renderInputField("HMAC", decodedKeys.hmac || "", "hmac", true)} 170 | {decodedKeys.authTag && renderInputField("Auth Tag", decodedKeys.authTag || "", "authTag", true)} 171 | {renderInputField("KEM Ciphertext", decodedKeys.kemCiphertext || "", "kemCiphertext", true)} 172 |
173 |
174 | ); 175 | }; 176 | 177 | export default ViewUnpackedKeys; -------------------------------------------------------------------------------- /electron/main/utils/extractHiddenData.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import crypto from 'crypto'; 4 | import { PNG } from 'pngjs'; 5 | import { AesKeySlice, generateKey, getIVLength } from '../utils/crypto'; 6 | import { app } from 'electron'; 7 | import { safeWriteLog } from './writeLog'; 8 | import { loadKemKeys } from './KeyService'; 9 | import { MlKem768 } from 'mlkem'; 10 | 11 | const kem = new MlKem768(); 12 | 13 | export function unpackFiles(buffer: Buffer, outputPath: string): string[] { 14 | safeWriteLog(`[unpackFiles] Buffer size: ${buffer.length}`); 15 | const extractedFiles: string[] = []; 16 | 17 | try { 18 | let offset = 0; 19 | const totalFiles = buffer.readUInt16BE(offset); 20 | offset += 2; 21 | 22 | safeWriteLog(`[unpackFiles] Total files embedded: ${totalFiles}`); 23 | 24 | for (let i = 0; i < totalFiles; i++) { 25 | safeWriteLog(`[unpackFiles] Extracting file ${i + 1}/${totalFiles}`); 26 | 27 | const filenameLength = buffer.readUInt16BE(offset); 28 | offset += 2; 29 | 30 | const filename = buffer.slice(offset, offset + filenameLength).toString('utf-8'); 31 | offset += filenameLength; 32 | 33 | const fileSize = buffer.readUInt32BE(offset); 34 | offset += 4; 35 | 36 | const fileData = buffer.slice(offset, offset + fileSize); 37 | offset += fileSize; 38 | 39 | const finalPath = path.join(outputPath, filename); 40 | const dir = path.dirname(finalPath); 41 | fs.mkdirSync(dir, { recursive: true }); 42 | fs.writeFileSync(finalPath, fileData); 43 | safeWriteLog(`[unpackFiles] File extracted to: ${finalPath}`); 44 | 45 | extractedFiles.push(finalPath); 46 | } 47 | 48 | return extractedFiles; 49 | } catch (err) { 50 | console.error(`[unpackFiles] Error:`, err); 51 | throw err; 52 | } 53 | } 54 | 55 | export default async function extractHiddenData( 56 | imagePath: string, 57 | method: string 58 | ): Promise<{ response: string; files: string[] }> { 59 | safeWriteLog(`[EXTRACT] Image path: ${imagePath}`); 60 | safeWriteLog(`[EXTRACT] Method: ${method}`); 61 | 62 | const { PRIVATE_KEY } = await loadKemKeys(); 63 | 64 | // Create temporary directory 65 | const tempDir = path.join(app.getPath('temp'), 'arocrypt-extraction'); 66 | if (!fs.existsSync(tempDir)) { 67 | fs.mkdirSync(tempDir, { recursive: true }); 68 | } 69 | 70 | return new Promise((resolve, reject) => { 71 | const imageStream = fs.createReadStream(imagePath); 72 | const png = new PNG(); 73 | 74 | png.on('parsed', async function () { 75 | try { 76 | safeWriteLog(`[EXTRACT] PNG parsed`); 77 | 78 | const bitData: number[] = []; 79 | for (let i = 0; i < this.data.length; i++) { 80 | bitData.push(this.data[i] & 1); 81 | } 82 | 83 | const byteData = []; 84 | for (let i = 0; i < bitData.length; i += 8) { 85 | const byte = bitData 86 | .slice(i, i + 8) 87 | .reduce((acc, bit, j) => acc | (bit << (7 - j)), 0); 88 | byteData.push(byte); 89 | } 90 | 91 | const fullBuffer = Buffer.from(byteData); 92 | const payloadBase64 = fullBuffer.toString('utf8').trim(); 93 | const payload: { 94 | content: string; 95 | iv: string; 96 | salt: string; 97 | kemCiphertext?: string | null; 98 | authTag?: string; 99 | hmac?: string; 100 | } = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8')); 101 | 102 | const ivBuffer = Buffer.from(payload.iv, 'hex'); 103 | const saltBuffer = Buffer.from(payload.salt, 'hex'); 104 | const encryptedData = Buffer.from(payload.content, 'hex'); 105 | const kemCiphertextBuf = payload.kemCiphertext 106 | ? Uint8Array.from(Buffer.from(payload.kemCiphertext, 'base64')) 107 | : null; 108 | const authTag = payload.authTag ? Buffer.from(payload.authTag, 'hex') : null; 109 | 110 | // Derive AES key 111 | let originalKeyForKdf: string; 112 | if (kemCiphertextBuf) { 113 | try { 114 | const sharedSecret = await kem.decap( 115 | kemCiphertextBuf, 116 | Uint8Array.from(Buffer.from(PRIVATE_KEY!, 'base64')) 117 | ); 118 | originalKeyForKdf = Buffer.from(sharedSecret).toString(); 119 | } catch (e) { 120 | safeWriteLog(`[EXTRACT] KEM decapsulation failed: ${(e as Error).message}`); 121 | throw e; 122 | } 123 | } else { 124 | originalKeyForKdf = PRIVATE_KEY!; 125 | } 126 | 127 | const aesKey = generateKey({ 128 | originalKey: originalKeyForKdf, 129 | method, 130 | salt: saltBuffer, 131 | }); 132 | 133 | // Verify HMAC if non-AEAD 134 | if (!/gcm|chacha/i.test(method) && payload.hmac) { 135 | const hmacKey = crypto.createHash('sha256').update(AesKeySlice(method, aesKey)).digest(); 136 | const hmac = crypto.createHmac('sha256', hmacKey); 137 | hmac.update(ivBuffer); 138 | hmac.update(saltBuffer); 139 | hmac.update(encryptedData); 140 | if (payload.kemCiphertext) hmac.update(Buffer.from(payload.kemCiphertext, 'base64')); 141 | const digest = hmac.digest('hex'); 142 | if (digest !== payload.hmac) throw new Error('HMAC validation failed'); 143 | } 144 | 145 | const decipher = crypto.createDecipheriv(method, AesKeySlice(method, aesKey), ivBuffer); 146 | if (authTag) (decipher as crypto.DecipherGCM).setAuthTag(authTag); 147 | 148 | const decryptedBuffer = Buffer.concat([decipher.update(encryptedData), decipher.final()]); 149 | const files = unpackFiles(decryptedBuffer, tempDir); 150 | 151 | safeWriteLog(`[EXTRACT] Extraction complete. Files: ${files}`); 152 | resolve({ response: 'OK', files }); 153 | } catch (err) { 154 | console.error(`[EXTRACT] Error during extraction/decryption:`, err); 155 | reject({ response: 'BAD', error: err }); 156 | } 157 | }); 158 | 159 | png.on('error', err => { 160 | console.error(`[EXTRACT] PNG error:`, err); 161 | reject({ response: 'BAD', error: err }); 162 | }); 163 | 164 | imageStream.pipe(png); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /src/Routes/TextDecryption.tsx: -------------------------------------------------------------------------------- 1 | import { RootState } from "@/store"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | import { Trans, useTranslation } from "react-i18next"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { setPackedKeys, setDecryptedText } from "@/store/decryptionSlice"; 6 | import BottomInfo from "@/Components/BottomInfo"; 7 | import { useToast } from "@/Context/ToastContext"; 8 | import { useClickOutside, useKeyPress } from "react-haiku"; 9 | import SwitchToggler from "@/Components/ui/SwitchToggler"; 10 | import HistoryModal from "@/Components/HistoryModal"; 11 | import { CheckAlgorithm } from "@/Utils/AlgorithmUtil"; 12 | import getDeviceOS from "@/Utils/getDeviceOS"; 13 | 14 | const Decryption: React.FC = () => { 15 | const toast = useToast(); 16 | const { t } = useTranslation(); 17 | const dispatch = useDispatch(); 18 | const { packedKeys, decrypted_text } = useSelector( 19 | (state: RootState) => state.decryption 20 | ); 21 | const [isButtonDisabled, setIsButtonDisabled] = useState(false); 22 | const osType = getDeviceOS(); 23 | 24 | const de_method = localStorage.getItem("decryptionMethod"); 25 | 26 | const handleDecrypt = async () => { 27 | try { 28 | CheckAlgorithm(); 29 | 30 | const decryptParams = { 31 | packedKeys: packedKeys, 32 | method: de_method, 33 | isSaveHistory: isSaveHistory, 34 | }; 35 | 36 | const decryptedResult = await window.electronAPI.decrypt( 37 | decryptParams as any 38 | ); 39 | 40 | if (decryptedResult === "invalid") { 41 | dispatch(setDecryptedText("")); 42 | setIsButtonDisabled(true); 43 | toast(0, t('toast.text_decryption_msg'), t('toast.text_decryption_title')) 44 | return; 45 | } 46 | 47 | dispatch(setDecryptedText(decryptedResult)); 48 | setIsButtonDisabled(true); 49 | } catch (error: any) { 50 | toast(0, `Unexpected Error has Occurred: ${error}`, "Unexpected Error") 51 | } 52 | }; 53 | 54 | const handleTypeSecurityKey = (e: any) => { 55 | dispatch(setPackedKeys(e.target.value)); 56 | dispatch(setDecryptedText("")); 57 | setIsButtonDisabled(false); 58 | setIsButtonDisabled(false); 59 | }; 60 | 61 | const combination = () => { 62 | if (packedKeys && !isButtonDisabled) { 63 | handleDecrypt(); 64 | } 65 | } 66 | 67 | useKeyPress(['Control', 'D'], combination); 68 | useKeyPress(['Meta', 'D'], combination); 69 | 70 | /* info: Config Functions */ 71 | 72 | // Dropdown and Modal 73 | const [isHistoryModal, setHistoryModal] = useState(false); 74 | const [isConfigOpen, setConfigOpen] = useState(false); 75 | const ConfigRef = useRef(null) 76 | 77 | useClickOutside(ConfigRef, () => setConfigOpen(false)); 78 | 79 | // Configs 80 | const [isSaveHistory, setSaveHistory] = useState(false); 81 | 82 | useEffect(() => { 83 | const contentHistory = localStorage.getItem('logs.dtext') === 'true'; 84 | 85 | setSaveHistory(contentHistory); 86 | }, [isSaveHistory]) 87 | 88 | const HandleConfigOperation = (config: string) => { 89 | switch (config) { 90 | case "logs": 91 | localStorage.setItem('logs.dtext', `${isSaveHistory ? false : true}`) 92 | setSaveHistory(!isSaveHistory) 93 | break; 94 | } 95 | } 96 | 97 | /* info: Config Functions {END} */ 98 | 99 | return ( 100 | <> 101 |
102 |

{t("text_decryption")}

103 |
104 | 110 |
111 | 117 |
118 |

{t("config.dtext")}

119 | 120 |
HandleConfigOperation("logs")}> 121 |
122 |

{t("config.history.title")}

123 |

{t("config.history.desc")}

124 |
125 | HandleConfigOperation("logs")} /> 126 |
127 |
128 |
129 |
130 |
131 | 132 | 140 |
141 |
142 | 143 | 150 |
151 | 166 | 167 |
168 | 169 | 170 | ); 171 | }; 172 | 173 | export default Decryption; 174 | -------------------------------------------------------------------------------- /electron/main/utils/hideDataInImage.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import crypto from "crypto"; 4 | import { AesKeySlice, generateKey, getIVLength } from "../utils/crypto"; 5 | import { PNG } from "pngjs"; 6 | import { safeWriteLog } from "./writeLog"; 7 | import { BrowserWindow, dialog } from "electron"; 8 | import { MlKem768 } from "mlkem"; 9 | import { loadKemKeys } from "./KeyService"; 10 | 11 | const kem = new MlKem768(); 12 | 13 | function packFiles(files: string[]): Buffer { 14 | safeWriteLog(`[packFiles] Start packing ${files.length} file(s)`); 15 | const buffers: Buffer[] = []; 16 | 17 | const fileCountBuffer = Buffer.alloc(2); 18 | fileCountBuffer.writeUInt16BE(files.length, 0); 19 | buffers.push(fileCountBuffer); 20 | 21 | for (const filePath of files) { 22 | const data = fs.readFileSync(filePath); 23 | const filename = path.basename(filePath); 24 | const nameBuffer = Buffer.from(filename, "utf-8"); 25 | 26 | const nameLength = Buffer.alloc(2); 27 | nameLength.writeUInt16BE(nameBuffer.length, 0); 28 | 29 | const sizeBuffer = Buffer.alloc(4); 30 | sizeBuffer.writeUInt32BE(data.length, 0); 31 | 32 | buffers.push(nameLength, nameBuffer, sizeBuffer, data); 33 | } 34 | 35 | const result = Buffer.concat(buffers); 36 | safeWriteLog(`[packFiles] Final packed buffer size: ${result.length}`); 37 | return result; 38 | } 39 | 40 | export async function embedDataInImage( 41 | imagePath: string, 42 | payloadBuffer: Buffer, 43 | outputPath: string 44 | ): Promise { 45 | safeWriteLog("[embedDataInImage] Embedding data into image..."); 46 | return new Promise((resolve, reject) => { 47 | fs.createReadStream(imagePath) 48 | .pipe(new PNG()) 49 | .on("parsed", function () { 50 | const bitArray: number[] = []; 51 | for (let byte of payloadBuffer) { 52 | for (let i = 7; i >= 0; i--) { 53 | bitArray.push((byte >> i) & 1); 54 | } 55 | } 56 | 57 | safeWriteLog( 58 | `[embedDataInImage] Image size: ${(this.width, "x", this.height)}` 59 | ); 60 | safeWriteLog( 61 | `[embedDataInImage] Payload size (bytes): ${payloadBuffer.length}` 62 | ); 63 | safeWriteLog( 64 | `[embedDataInImage] Payload size (bits): ${bitArray.length}` 65 | ); 66 | safeWriteLog( 67 | `[embedDataInImage] PNG raw buffer size: ${this.data.length}` 68 | ); 69 | 70 | if (bitArray.length > this.data.length) { 71 | return reject("payload_too_large"); 72 | } 73 | 74 | for (let i = 0; i < bitArray.length; i++) { 75 | this.data[i] = (this.data[i] & 0b11111110) | bitArray[i]; 76 | } 77 | 78 | this.pack() 79 | .pipe(fs.createWriteStream(outputPath)) 80 | .on("finish", () => { 81 | safeWriteLog( 82 | "[embedDataInImage] Embedding completed successfully" 83 | ); 84 | resolve(); 85 | }); 86 | }) 87 | .on("error", (err) => { 88 | console.error("[embedDataInImage] PNG parsing error:", err); 89 | reject(err); 90 | }); 91 | }); 92 | } 93 | 94 | async function canEmbedDataInImage(imagePath: string, payloadBytes: number): Promise { 95 | return new Promise((resolve, reject) => { 96 | fs.createReadStream(imagePath) 97 | .pipe(new PNG()) 98 | .on("parsed", function () { 99 | const requiredBits = payloadBytes * 8; 100 | const availableBits = this.data.length; 101 | 102 | safeWriteLog(`[canEmbedDataInImage] Required bits: ${requiredBits}`); 103 | safeWriteLog(`[canEmbedDataInImage] Available bits: ${availableBits}`); 104 | 105 | resolve(requiredBits <= availableBits); 106 | }) 107 | .on("error", (err) => { 108 | console.error("[canEmbedDataInImage] PNG parsing error:", err); 109 | reject(err); 110 | }); 111 | }); 112 | } 113 | 114 | export default async function hideDataInImage( 115 | win: BrowserWindow, 116 | imagePath: string, 117 | secretFilesPaths: string[], 118 | method: string, 119 | isShareable: boolean 120 | ): Promise<{ outputPath: string; response: string }> { 121 | try { 122 | safeWriteLog("[hideDataInImage] Start"); 123 | safeWriteLog(`[hideDataInImage] Files: ${secretFilesPaths}`); 124 | safeWriteLog(`[hideDataInImage] Method: ${method}`); 125 | 126 | const { PUBLIC_KEY, RECIPIENT_KEY } = await loadKemKeys(); 127 | const ivLength = getIVLength(method); 128 | if (!ivLength) throw new Error("Invalid IV length for selected algorithm."); 129 | const iv = crypto.randomBytes(ivLength); 130 | const salt = crypto.randomBytes(16); 131 | 132 | let aesKey: Buffer; 133 | let kemCiphertext: string | null = null; 134 | 135 | if ((isShareable && !RECIPIENT_KEY) || (!isShareable && !PUBLIC_KEY)) { 136 | throw new Error("PROBLEM WITH KEYS!"); 137 | } 138 | 139 | if (isShareable) { 140 | const recipientKeyUint8 = Uint8Array.from(Buffer.from(RECIPIENT_KEY, "base64")); 141 | const [ciphertext, sharedSecret] = await kem.encap(recipientKeyUint8); 142 | kemCiphertext = Buffer.from(ciphertext).toString("base64"); 143 | 144 | aesKey = generateKey({ 145 | originalKey: Buffer.from(sharedSecret).toString(), 146 | method, 147 | salt, 148 | }); 149 | } else { 150 | const publicKeyUint8 = Uint8Array.from(Buffer.from(PUBLIC_KEY, "base64")); 151 | const [ciphertext, sharedSecret] = await kem.encap(publicKeyUint8); 152 | kemCiphertext = Buffer.from(ciphertext).toString("base64"); 153 | 154 | aesKey = generateKey({ 155 | originalKey: Buffer.from(sharedSecret).toString(), 156 | method, 157 | salt, 158 | }); 159 | } 160 | 161 | const key = AesKeySlice(method, aesKey); 162 | const cipher = crypto.createCipheriv(method, key, iv); 163 | 164 | const packedData = packFiles(secretFilesPaths); 165 | let encrypted = cipher.update(packedData); 166 | encrypted = Buffer.concat([encrypted, cipher.final()]); 167 | 168 | const payload: any = { 169 | content: encrypted.toString("hex"), 170 | iv: iv.toString("hex"), 171 | salt: salt.toString("hex"), 172 | kemCiphertext, 173 | }; 174 | 175 | if (/gcm|chacha/i.test(method)) { 176 | payload.authTag = (cipher as crypto.CipherGCM).getAuthTag().toString("hex"); 177 | } else { 178 | const hmacKey = crypto.createHash("sha256").update(key).digest(); 179 | const hmac = crypto.createHmac("sha256", hmacKey); 180 | hmac.update(iv); 181 | hmac.update(salt); 182 | hmac.update(encrypted); 183 | if (kemCiphertext) hmac.update(kemCiphertext); 184 | payload.hmac = hmac.digest("hex"); 185 | } 186 | 187 | const finalPayload = Buffer.from(JSON.stringify(payload)).toString("base64"); 188 | 189 | const canFit = await canEmbedDataInImage(imagePath, Buffer.byteLength(finalPayload)); 190 | if (!canFit) { 191 | safeWriteLog("[EMBED] Payload too large. (/skipping save dialog/)"); 192 | return { outputPath: "", response: "payload_too_large" }; 193 | } 194 | 195 | const originalFilename = path.basename(imagePath); 196 | const { canceled, filePath: finalOutputPath } = await dialog.showSaveDialog(win, { 197 | title: "Save Stego Image", 198 | defaultPath: originalFilename, 199 | filters: [{ name: "Images", extensions: ["png"] }], 200 | }); 201 | 202 | if (canceled || !finalOutputPath) return { outputPath: "", response: "canceled" }; 203 | 204 | await embedDataInImage(imagePath, Buffer.from(finalPayload), finalOutputPath); 205 | 206 | safeWriteLog("[EMBED] All done!"); 207 | return { outputPath: finalOutputPath, response: "OK" }; 208 | } catch (error) { 209 | console.error("[EMBED] Error:", error); 210 | throw error; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "encryption": "Encryption", 3 | "decryption": "Decryption", 4 | "text": "Text", 5 | "file": "File", 6 | "encrypt_file": "Encrypt File", 7 | "decrypt_file": "Decrypt File", 8 | "image": "Image", 9 | "image_steganography": "Steganography", 10 | "embed_data": "Embed Data", 11 | "extract_data": "Extract Data", 12 | "text_encryption": "Text Encryption", 13 | "text_decryption": "Text Decryption", 14 | "file_encryption": "File Encryption", 15 | "file_decryption": "File Decryption", 16 | "text_to_encrypt": "Enter Text to Encrypt", 17 | "decrypted_text": "Decrypted Text Output", 18 | "packed_package": "Data Package", 19 | "no_files_selected": "No File(s) Selected", 20 | "select_files_to_encrypt": "Select File(s) to Encrypt", 21 | "select_files_to_decrypt": "Select File(s) to Decrypt", 22 | "files_selected": "{{count}} File(s) Selected", 23 | "encrypt_text": "Encrypt Text", 24 | "decrypt_text": "Decrypt Text", 25 | "encrypt_files": "Encrypt File(s)", 26 | "decrypt_files": "Decrypt File(s)", 27 | "encrypting_files": "Encrypting File(s)...", 28 | "decrypting_files": "Decrypting File(s)...", 29 | "encryption_failed": "Encryption Failed", 30 | "decryption_failed": "Decryption Failed", 31 | "decryption_failed_info": "Decryption failed due to a damaged file, an incorrect private key, or an incompatible decryption method.", 32 | "original_files": "Original File(s)", 33 | "decrypted_files": "Decrypted File(s)", 34 | "encrypted_files": "Encrypted File(s)", 35 | "extracted_files": "Extracted File(s)", 36 | "data_embedding": "Data Embedding", 37 | "embedded_files": "Embedded File(s)", 38 | "embedding_files": "Embedding File(s)...", 39 | "data_embedding_info": "Embedding encrypts files automatically using your chosen algorithm.", 40 | "data_extractor": "Data Extractor", 41 | "extracting_files": "Extracting File(s)...", 42 | "choose_image_to_extract": "Select an Image(s) to Extract Hidden Data", 43 | "choose_image_to_embed": "Select an Image to Embed Data", 44 | "embed_data_info": "Only PNG files are supported to maintain data integrity and security.", 45 | "choose_files_to_embed": "Select File(s) to Embed", 46 | "unable_embed_data": "Failed to Embed Data into Image", 47 | "unable_extract_data": "Failed to Extract Hidden Data", 48 | "payload_too_large": "The selected file(s) are too large to be embedded within the image!", 49 | "result": "Result", 50 | "embed_data_error": "Unable to embed data. Ensure the payload fits within the image’s capacity and the selected files are within acceptable limits.", 51 | "extract_data_error": "Extraction failed. Possible reasons include an invalid image, incorrect private key, or incompatible encryption method.", 52 | "settings": "Settings", 53 | "security": "Security", 54 | "appearance": "Appearance", 55 | "encryption_algorithm": "Encryption Algorithm", 56 | "encryption_algorithm_info": "The algorithm used for encrypting files, text, and data embedded in images.", 57 | "decryption_algorithm": "Decryption Algorithm", 58 | "decryption_algorithm_info": "The algorithm used for decrypting files, text, and embedded data.", 59 | "setup_keys": "Setup Keys", 60 | "private_key": "Private Key", 61 | "public_key": "Public Key", 62 | "recipient_public_key": "Recipient Public Key", 63 | "manage_keys": "Manage Keys", 64 | "manage_keys_info": "These keys are used for encryption, decryption, and verification.", 65 | "apply_agreement_info": "I understand that clicking \"Apply\" erases history and unsaved keys mean lost files.", 66 | "create_key_pair": "Create Key Pair", 67 | "quantum_safe": "Quantum-Safe: KYBER", 68 | "dont_share": "DON'T SHARE!", 69 | "recommended": "Recommended", 70 | "legacy": "Legacy", 71 | "stream_mode": "Stream Mode", 72 | "cancel": "Cancel", 73 | "apply": "Apply", 74 | "change": "Change", 75 | "interface_language": "Interface Language", 76 | "interface_language_info": "Select the language for the application’s interface and messages.", 77 | "image_file_selected": "Image File Selected", 78 | "change_private_key": "Change Private Key", 79 | "set_private_key": "Set Private Key", 80 | "developed_by": "Developed By", 81 | "source_code": "Source Code", 82 | "report_issue": "Report an Issue", 83 | "support_project": "Support Project", 84 | "about": "About AroCrypt", 85 | "arocrypt_site": "Official Site", 86 | "about_page_info": "AroCrypt is a software for encryption and decryption text and files, image steganograhpy with auto encryption.", 87 | "about_dev_info": "A solo project developed by", 88 | "update_required": "Update Required!", 89 | "update_info": "A newer version of AroCrypt is available. Upgrading is recommended to maintain stability and prevent known issues.", 90 | "update_now": "Update Now!", 91 | "downloading": "Downloading...", 92 | "open_latest_version": "Download Latest Version Manually", 93 | "app_theme": { 94 | "title": "Application Theme", 95 | "desc": "Customize the application's interface to suit your visual preferences.", 96 | "light": "Light", 97 | "dark": "Dark" 98 | }, 99 | "view_unpacked": "View Unpacked", 100 | "unpacked_package": "Unpacked Data", 101 | "safe_to_share": "This package is public and are safe to share.", 102 | "config": { 103 | "etext": "Text Encryption Settings", 104 | "dtext": "Text Decryption Settings", 105 | "efile": "File Encryption Settings", 106 | "dfile": "File Decryption Settings", 107 | "hide_data": "Data Embedding Settings", 108 | "extract_data": "Data Extraction Settings", 109 | "history": { 110 | "title": "Operation History", 111 | "desc": "Keep a detailed log of all actions performed for reference." 112 | }, 113 | "shareable": { 114 | "title": "Shareable Mode", 115 | "desc": "Use recipient’s key so only they can securely decrypt the content." 116 | }, 117 | "delete_source": { 118 | "title": "Auto-Remove Source", 119 | "desc": "Erase original files automatically once results are safely created." 120 | }, 121 | "single_output": { 122 | "title": "Unified Output", 123 | "desc": "Store all results together in one chosen destination folder." 124 | } 125 | }, 126 | "logs": { 127 | "etext": "Text Encryption Logs", 128 | "efile": "File Encryption Logs", 129 | "dtext": "Text Decryption Logs", 130 | "dfile": "File Decryption Logs", 131 | "steg_in": "Data Embedding Logs", 132 | "steg_out": "Extraction Logs", 133 | "logs": "Logs", 134 | "logging_disabled": "Logging disabled. Upcoming operations won't be tracked.", 135 | "no_log": "No Logs Found.", 136 | "delete_log": "Delete Log", 137 | "table": { 138 | "status": "Status", 139 | "success": "Success", 140 | "canceled": "Canceled", 141 | "fail": "Error", 142 | "time": "Time", 143 | "input": "Input", 144 | "output": "Output", 145 | "size": "Size", 146 | "algo": "Algorithm", 147 | "duration": "Duration" 148 | } 149 | }, 150 | "using_algo": "Using <1>{{method}} Algorithm", 151 | "encrypt_all_files": "Encrypt All Files", 152 | "decrypt_all_files": "Decrypt All Files", 153 | "extract_all_files": "Extract All Files", 154 | "secret_files_selected": "{{count}} Secret File(s) Selected", 155 | "toast": { 156 | "text_decryption_title": "Text Decryption Error", 157 | "text_decryption_msg": "Decryption failed. This may occur if the private key is incorrect or the wrong algorithm is selected.", 158 | "file_limit_title": "File Selection Limit", 159 | "file_limit_msg": "You can select a maximum of 20 files.", 160 | "unexpected_error_title": "Unexpected Error", 161 | "unexpected_error_msg": "An unknown error has occurred. Please try again.", 162 | "file_encryption_title": "File Encryption Error", 163 | "file_encryption_msg": "Unable to encrypt the file. Please try again.", 164 | "file_decryption_title": "File Decryption Error", 165 | "file_decryption_msg": "Decryption failed. This may occur if the private key is incorrect, the wrong algorithm is selected, the file is corrupted, or permission is denied.", 166 | "payload_too_large_title": "Payload Exceeds Limit", 167 | "payload_too_large_msg": "The selected file(s) are too large to embed.", 168 | "data_embedding_title": "Embedding Error", 169 | "data_embedding_msg": "Failed to embed the data. Please try again.", 170 | "data_extraction_title": "Extraction Error", 171 | "data_extraction_msg": "Extraction failed. This may occur if the private key is incorrect, the wrong algorithm is selected, or no hidden data exists in the file.", 172 | "keys_saved_title": "Changes Saved", 173 | "keys_saved_msg": "Keys have been saved successfully.", 174 | "logs_deleted_title": "Logs Cleared", 175 | "logs_deleted": "All logs have been cleared from this section." 176 | } 177 | } -------------------------------------------------------------------------------- /src/Locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "encryption": "Шифрование", 3 | "decryption": "Расшифровка", 4 | "text": "Текст", 5 | "file": "Файл", 6 | "encrypt_file": "Зашифровать файл", 7 | "decrypt_file": "Расшифровать файл", 8 | "image": "Изображение", 9 | "image_steganography": "Стеганография", 10 | "embed_data": "Встроить данные", 11 | "extract_data": "Извлечь данные", 12 | "text_encryption": "Шифрование текста", 13 | "text_decryption": "Расшифровка текста", 14 | "file_encryption": "Шифрование файла", 15 | "file_decryption": "Расшифровка файла", 16 | "text_to_encrypt": "Введите текст для шифрования", 17 | "decrypted_text": "Расшифрованный текст", 18 | "packed_package": "Пакет данных", 19 | "no_files_selected": "Файл(ы) не выбраны", 20 | "select_files_to_encrypt": "Выберите файл(ы) для шифрования", 21 | "select_files_to_decrypt": "Выберите файл(ы) для расшифровки", 22 | "files_selected": "Выбрано файлов: {{count}}", 23 | "encrypt_text": "Зашифровать текст", 24 | "decrypt_text": "Расшифровать текст", 25 | "encrypt_files": "Зашифровать файл(ы)", 26 | "decrypt_files": "Расшифровать файл(ы)", 27 | "encrypting_files": "Шифрование файлов...", 28 | "decrypting_files": "Расшифровка файлов...", 29 | "encryption_failed": "Ошибка шифрования", 30 | "decryption_failed": "Ошибка расшифровки", 31 | "decryption_failed_info": "Расшифровка не удалась: файл повреждён, ключ неверен или выбран неправильный алгоритм.", 32 | "original_files": "Исходные файл(ы)", 33 | "decrypted_files": "Расшифрованные файл(ы)", 34 | "encrypted_files": "Зашифрованные файл(ы)", 35 | "extracted_files": "Извлечённые файл(ы)", 36 | "data_embedding": "Встраивание данных", 37 | "embedded_files": "Встроенные файл(ы)", 38 | "embedding_files": "Встраивание файлов...", 39 | "data_embedding_info": "Файл(ы) будут автоматически зашифрованы выбранным алгоритмом.", 40 | "data_extractor": "Извлечение данных", 41 | "extracting_files": "Извлечение файлов...", 42 | "choose_image_to_extract": "Выберите изображение для извлечения данных", 43 | "choose_image_to_embed": "Выберите изображение для встраивания данных", 44 | "embed_data_info": "Поддерживаются только PNG-файлы для безопасности и стабильности.", 45 | "choose_files_to_embed": "Выберите файл(ы) для встраивания", 46 | "unable_embed_data": "Не удалось встроить данные в изображение", 47 | "unable_extract_data": "Не удалось извлечь данные", 48 | "payload_too_large": "Выбранные файл(ы) слишком велики, чтобы их можно было встроить в изображение!", 49 | "result": "Результат", 50 | "embed_data_error": "Ошибка встраивания. Убедитесь, что данные помещаются в изображение и файлы допустимы.", 51 | "extract_data_error": "Ошибка извлечения. Причины: неверный ключ, неправильный алгоритм или повреждённое изображение.", 52 | "settings": "Настройки", 53 | "security": "Безопасность", 54 | "appearance": "Внешний вид", 55 | "encryption_algorithm": "Алгоритм шифрования", 56 | "encryption_algorithm_info": "Алгоритм, используемый для шифрования текста, файлов и данных в изображениях.", 57 | "decryption_algorithm": "Алгоритм расшифровки", 58 | "decryption_algorithm_info": "Алгоритм, используемый для расшифровки текста, файлов и встроенных данных.", 59 | "setup_keys": "Настройка ключей", 60 | "private_key": "Приватный ключ", 61 | "public_key": "Публичный ключ", 62 | "recipient_public_key": "Публичный ключ получателя", 63 | "manage_keys": "Управление ключами", 64 | "manage_keys_info": "Эти ключи используются для шифрования, дешифрования и проверки.", 65 | "apply_agreement_info": "Я понимаю, что нажатие «Применить» удаляет историю, а несохраненные ключи приведут к потере файлов.", 66 | "create_key_pair": "Создать пару ключей", 67 | "quantum_safe": "Квантово-устойчивый: KYBER", 68 | "dont_share": "НЕ РАСПРОСТРАНЯТЬ!", 69 | "recommended": "Рекомендуется", 70 | "legacy": "Устаревший", 71 | "stream_mode": "Потоковый режим", 72 | "cancel": "Отмена", 73 | "apply": "Применить", 74 | "change": "Изменить", 75 | "interface_language": "Язык интерфейса", 76 | "interface_language_info": "Выберите язык интерфейса и системного текста приложения.", 77 | "image_file_selected": "Изображение выбрано", 78 | "change_private_key": "Сменить приватный ключ", 79 | "set_private_key": "Установить приватный ключ", 80 | "developed_by": "Разработано", 81 | "source_code": "Исходный код", 82 | "report_issue": "Сообщить об ошибке", 83 | "support_project": "Поддержать проект", 84 | "about": "О приложении", 85 | "arocrypt_site": "Официальный Сайт", 86 | "about_page_info": "AroCrypt — это программа для шифрования и расшифровки текста, файлов и стеганографии изображений с автошифрованием.", 87 | "about_dev_info": "Соло-проект от", 88 | "update_required": "Требуется обновление!", 89 | "update_info": "Доступна новая версия AroCrypt. Рекомендуется обновиться для стабильной работы и предотвращения известных ошибок.", 90 | "update_now": "Обновить сейчас!", 91 | "downloading": "Загрузка...", 92 | "open_latest_version": "Загрузите последнюю версию вручную", 93 | "app_theme": { 94 | "title": "Тема приложения", 95 | "desc": "Настройте интерфейс под ваш стиль и визуальное удобство.", 96 | "light": "Светлая", 97 | "dark": "Тёмная" 98 | }, 99 | "view_unpacked": "Просмотреть Распакованный", 100 | "unpacked_package": "Распакованные данные", 101 | "safe_to_share": "Пакет — безопасен для передачи.", 102 | "config": { 103 | "etext": "Настройки шифрования текста", 104 | "dtext": "Настройки расшифровки текста", 105 | "efile": "Настройки шифрования файлов", 106 | "dfile": "Настройки расшифровки файлов", 107 | "hide_data": "Настройки встраивания данных", 108 | "extract_data": "Настройки извлечения данных", 109 | "history": { 110 | "title": "История операций", 111 | "desc": "Ведите подробный журнал всех выполненных действий для справки." 112 | }, 113 | "shareable": { 114 | "title": "Режим общего доступа", 115 | "desc": "Используйте ключ получателя, чтобы только он расшифровал содержимое." 116 | }, 117 | "delete_source": { 118 | "title": "Авто-удаление источника", 119 | "desc": "Файлы стираются после безопасного создания результата." 120 | }, 121 | "single_output": { 122 | "title": "Единый вывод", 123 | "desc": "Сохраняйте все результаты вместе в выбранной папке назначения." 124 | } 125 | }, 126 | "logs": { 127 | "etext": "Журналы шифрования текста", 128 | "efile": "Журналы шифрования файлов", 129 | "dtext": "Журналы расшифровки текста", 130 | "dfile": "Журналы расшифровки файлов", 131 | "steg_in": "Журналы встраивания данных", 132 | "steg_out": "Журналы извлечения", 133 | "logs": "Журналы", 134 | "logging_disabled": "Журналирование отключено. Предстоящие операции не будут отслежены.", 135 | "no_log": "Журналы не найдены.", 136 | "delete_log": "Удалить журнал", 137 | "table": { 138 | "status": "Статус", 139 | "success": "Успешно", 140 | "canceled": "Отменено", 141 | "fail": "Ошибка", 142 | "time": "Время", 143 | "input": "Ввод", 144 | "output": "Вывод", 145 | "size": "Размер", 146 | "algo": "Алгоритм", 147 | "duration": "Длительность" 148 | } 149 | }, 150 | "using_algo": "Используя <1>{{method}} Алгоритм", 151 | "encrypt_all_files": "Зашифровать Все Файлы", 152 | "decrypt_all_files": "Расшифровать Все Файлы", 153 | "extract_all_files": "Извлечь Все Файлы", 154 | "secret_files_selected": "Выбрано {{count}} секретных файл(ов)", 155 | "toast": { 156 | "text_decryption_title": "Ошибка расшифровки текста", 157 | "text_decryption_msg": "Расшифровка не выполнена. Возможные причины: неверный приватный ключ или выбран неправильный алгоритм.", 158 | "file_limit_title": "Лимит файлов", 159 | "file_limit_msg": "Можно выбрать не более 20 файлов.", 160 | "unexpected_error_title": "Неизвестная ошибка", 161 | "unexpected_error_msg": "Произошла непредвиденная ошибка. Попробуйте снова.", 162 | "file_encryption_title": "Ошибка шифрования файла", 163 | "file_encryption_msg": "Не удалось зашифровать файл. Попробуйте снова.", 164 | "file_decryption_title": "Ошибка расшифровки файла", 165 | "file_decryption_msg": "Расшифровка не выполнена. Возможные причины: неверный приватный ключ, выбран неправильный алгоритм, файл поврежден или отсутствует доступ.", 166 | "payload_too_large_title": "Превышен размер данных", 167 | "payload_too_large_msg": "Выбранные файлы слишком большие для встраивания.", 168 | "data_embedding_title": "Ошибка встраивания", 169 | "data_embedding_msg": "Не удалось встроить данные. Попробуйте снова.", 170 | "data_extraction_title": "Ошибка извлечения", 171 | "data_extraction_msg": "Извлечение не выполнено. Возможные причины: неверный приватный ключ, выбран неправильный алгоритм или в файле нет скрытых данных.", 172 | "keys_saved_title": "Изменения сохранены", 173 | "keys_saved_msg": "Ключи успешно сохранены.", 174 | "logs_deleted_title": "Журнал очищен", 175 | "logs_deleted": "Все записи в этом разделе удалены." 176 | } 177 | } -------------------------------------------------------------------------------- /electron/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, contextBridge } from "electron"; 2 | 3 | interface FileResult { 4 | inputPath: string; 5 | output: string; 6 | } 7 | 8 | type KyberKeyPair = { 9 | pubkey: string; 10 | secret: string; 11 | }; 12 | 13 | interface ElectronAPI { 14 | encryptFile: ( 15 | filesPath: string[], 16 | method: string, 17 | isDeleteSource: boolean, 18 | isSaveHistory: boolean, 19 | isSingleOutput: boolean, 20 | isShareable: boolean 21 | ) => Promise; 22 | 23 | decryptFile: ( 24 | filesPath: string[], 25 | method: string, 26 | isDeleteSource: boolean, 27 | isSaveHistory: boolean, 28 | isSingleOutput: boolean 29 | ) => Promise; 30 | 31 | extractHiddenData: ( 32 | filesPath: string[], 33 | method: string, 34 | isDeleteSource: boolean, 35 | isSaveHistory: boolean, 36 | isSingleOutput: boolean 37 | ) => Promise; 38 | 39 | hideData: ( 40 | filesPath: string[], 41 | secretFilesPath: string[], 42 | method: string, 43 | isDeleteSource: boolean, 44 | isSaveHistory: boolean, 45 | isShareable: boolean 46 | ) => Promise; 47 | 48 | showSaveDialog: (options: { 49 | title?: string; 50 | defaultPath?: string; 51 | }) => Promise; 52 | 53 | openFileDialog: () => Promise; 54 | openFileDialogD: () => Promise; 55 | 56 | // # Args from Context Menu 57 | onFilesToDecrypt: (callback: (paths: string[]) => void) => void; 58 | onFilesToEncrypt: (callback: (paths: string[]) => void) => void; 59 | 60 | selectDataHiderImage: () => Promise; 61 | selectDataHiderSecretFiles: () => Promise; 62 | selectDataExtractorImage: () => Promise; 63 | 64 | // Keys 65 | noKeys: () => Promise; 66 | 67 | kyberKeyPair: () => Promise; 68 | saveKeys: (secret_key: string, public_key: string, recipient_key: string) => Promise; 69 | getKeys: () => Promise<{ secret_key: string; public_key: string }>; 70 | 71 | // Other Functions 72 | encrypt: (params: { text: string; method: string; isSaveHistory: boolean; isShareable: boolean }) => Promise<{ 73 | content: string; 74 | iv: string; 75 | salt: string; 76 | hmac: string; 77 | method: string; 78 | }>; 79 | decrypt: (params: { packedKeys: string; method: string; isSaveHistory: boolean }) => Promise; 80 | 81 | // Utility Methods 82 | copyToClipboard: (text: string) => Promise; 83 | openExternalLink: (url: string) => void; 84 | getAppVersion: () => Promise; 85 | getPlatform: () => Promise; 86 | 87 | getLogs: (table: string) => void; 88 | deleteLog: (params: { table: string; id: string }) => void; 89 | 90 | // Window Control 91 | maximizeWindow: () => Promise; 92 | onMaximize: (callback: (isMax: boolean) => void) => void; 93 | minimizeWindow: () => Promise; 94 | closeWindow: () => Promise; 95 | downloadUpdate?: () => Promise; 96 | openAboutWindow?: () => Promise; 97 | 98 | // Update Checking 99 | checkForUpdates: () => Promise; 100 | onUpdateAvailable: ( 101 | callback: (updateInfo: UpdateInfo & { isUpdateAvailable: boolean }) => void 102 | ) => void; 103 | onUpdateNotAvailable: ( 104 | callback: (updateInfo: UpdateInfo & { isUpdateAvailable: boolean }) => void 105 | ) => void; 106 | 107 | onUpdateDownloadProgress: ( 108 | callback: (progress: { 109 | percent: number; 110 | transferred: number; 111 | total: number; 112 | bytesPerSecond: number; 113 | }) => void 114 | ) => void; 115 | } 116 | 117 | interface UpdateInfo { 118 | version: string; 119 | files: Array<{ 120 | url: string; 121 | size: number; 122 | }>; 123 | path?: string; 124 | releaseDate?: string; 125 | } 126 | 127 | let bufferedFilesToDecrypt: string[] | null = null; 128 | let filesToDecryptCallback: ((paths: string[]) => void) | null = null; 129 | 130 | // --------- Expose some API to the Renderer process --------- 131 | const electronAPI: ElectronAPI = { 132 | encryptFile: ( 133 | filesPath: string[], 134 | method: string, 135 | isDeleteSource: boolean, 136 | isSaveHistory: boolean, 137 | isSingleOutput: boolean, 138 | isShareable: boolean 139 | ) => 140 | ipcRenderer.invoke( 141 | "encrypt-file", 142 | filesPath, 143 | method, 144 | isDeleteSource, 145 | isSaveHistory, 146 | isSingleOutput, 147 | isShareable 148 | ), 149 | 150 | decryptFile: ( 151 | filesPath: string[], 152 | method: string, 153 | isDeleteSource: boolean, 154 | isSaveHistory: boolean, 155 | isSingleOutput: boolean 156 | ) => 157 | ipcRenderer.invoke( 158 | "decrypt-file", 159 | filesPath, 160 | method, 161 | isDeleteSource, 162 | isSaveHistory, 163 | isSingleOutput 164 | ), 165 | 166 | extractHiddenData: ( 167 | filesPath: string[], 168 | method: string, 169 | isDeleteSource: boolean, 170 | isSaveHistory: boolean, 171 | isSingleOutput: boolean 172 | ) => 173 | ipcRenderer.invoke( 174 | "extract-data", 175 | filesPath, 176 | method, 177 | isDeleteSource, 178 | isSaveHistory, 179 | isSingleOutput 180 | ), 181 | 182 | hideData: ( 183 | filesPath: string[], 184 | secretFilesPath: string[], 185 | method: string, 186 | isDeleteSource: boolean, 187 | isSaveHistory: boolean, 188 | isShareable: boolean 189 | ) => 190 | ipcRenderer.invoke( 191 | "hide-data", 192 | filesPath, 193 | secretFilesPath, 194 | method, 195 | isDeleteSource, 196 | isSaveHistory, 197 | isShareable 198 | ), 199 | 200 | showSaveDialog: (options) => 201 | ipcRenderer.invoke("show-save-dialog", options).then((result) => { 202 | if (result.canceled) { 203 | throw new Error("Save dialog was canceled"); 204 | } 205 | return result.filePath; 206 | }), 207 | 208 | // # Args from Context Menu 209 | onFilesToDecrypt: (cb) => 210 | ipcRenderer.on("files-to-decrypt", (_e, files) => cb(files)), 211 | onFilesToEncrypt: (cb) => 212 | ipcRenderer.on("files-to-encrypt", (_e, files) => cb(files)), 213 | 214 | //Dialogs 215 | openFileDialog: () => ipcRenderer.invoke("open-file-dialog"), 216 | openFileDialogD: () => ipcRenderer.invoke("open-file-dialog-d"), 217 | 218 | selectDataHiderImage: () => ipcRenderer.invoke("select-image-datahider"), 219 | selectDataHiderSecretFiles: () => ipcRenderer.invoke("select-secret-files"), 220 | 221 | selectDataExtractorImage: () => 222 | ipcRenderer.invoke("select-data-extractor-image"), 223 | 224 | // Private and Public Keys 225 | 226 | noKeys: async () => { 227 | const key = await ipcRenderer.invoke("get-keys"); 228 | return key.length === 0; 229 | }, 230 | 231 | kyberKeyPair: async (): Promise => { 232 | const keys = await ipcRenderer.invoke("create-kyber-keys"); 233 | return keys as KyberKeyPair; 234 | }, 235 | 236 | saveKeys: (secret_key: string, public_key: string, recipient_key: string) => { 237 | return ipcRenderer.invoke("save-keys", secret_key, public_key, recipient_key); 238 | }, 239 | 240 | getKeys: () => ipcRenderer.invoke("get-keys"), 241 | 242 | // Other Functions 243 | encrypt: ({ text, method, isSaveHistory, isShareable }) => { 244 | return ipcRenderer.invoke("encrypt", { text, method, isSaveHistory, isShareable }); 245 | }, 246 | decrypt: ({ packedKeys, method, isSaveHistory }) => { 247 | return ipcRenderer.invoke("decrypt", { packedKeys, method, isSaveHistory }); 248 | }, 249 | 250 | // Utility Methods 251 | copyToClipboard: (text) => navigator.clipboard.writeText(text), 252 | openExternalLink: (url) => ipcRenderer.send("open-external-link", url), 253 | getAppVersion: () => ipcRenderer.invoke("get-app-version"), 254 | getPlatform: () => ipcRenderer.invoke("get-platform"), 255 | 256 | getLogs: (table) => ipcRenderer.invoke("get-logs", table), 257 | deleteLog: ({ table, id }) => ipcRenderer.invoke("delete-log", { table, id }), 258 | 259 | // Window Control 260 | maximizeWindow: () => ipcRenderer.invoke("maximize-window"), 261 | onMaximize: (callback: (isMax: boolean) => void) => { 262 | ipcRenderer.on("window-maximize", (_event, state: boolean) => 263 | callback(state) 264 | ); 265 | }, 266 | minimizeWindow: () => ipcRenderer.invoke("minimize-window"), 267 | closeWindow: () => ipcRenderer.invoke("close-window"), 268 | 269 | onUpdateAvailable: (callback) => 270 | ipcRenderer.on("update-available", (_event, updateInfo) => { 271 | callback({ ...updateInfo, isUpdateAvailable: true }); 272 | }), 273 | 274 | checkForUpdates: () => ipcRenderer.invoke("check-for-updates"), 275 | downloadUpdate: () => ipcRenderer.invoke("download-update"), 276 | openAboutWindow: () => ipcRenderer.invoke("open-about-window"), 277 | 278 | onUpdateNotAvailable: (callback) => 279 | ipcRenderer.on("update-not-available", (_event, updateInfo) => { 280 | callback({ ...updateInfo, isUpdateAvailable: false }); 281 | }), 282 | 283 | onUpdateDownloadProgress: ( 284 | callback: (progress: { 285 | percent: number; 286 | transferred: number; 287 | total: number; 288 | bytesPerSecond: number; 289 | }) => void 290 | ) => 291 | ipcRenderer.on("update-download-progress", (_event, progress) => 292 | callback(progress) 293 | ), 294 | }; 295 | 296 | contextBridge.exposeInMainWorld("electronAPI", electronAPI); 297 | -------------------------------------------------------------------------------- /src/Components/MainDropDown.tsx: -------------------------------------------------------------------------------- 1 | import useOpenLink from "@/Utils/openLink"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | interface dropdown { 5 | isShown: boolean; 6 | } 7 | 8 | export default function MainDropDown({ isShown }: dropdown): JSX.Element { 9 | const { t } = useTranslation(); 10 | 11 | const handleAboutApp = async () => { 12 | try { 13 | await window.electronAPI.openAboutWindow(); 14 | } catch (error) { 15 | console.error('Failed to close window:', error); 16 | } 17 | } 18 | 19 | return ( 20 |
23 |
24 |
25 | 36 | 47 | 58 |
59 | 60 |
61 | 72 | 81 |
82 | 83 |
84 | 93 | 102 |
103 |
104 |
105 | ); 106 | } 107 | --------------------------------------------------------------------------------