├── 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 |
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 |

12 |
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 |
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 |
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 |

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 |
7 |
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 |
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 |
7 |
8 |
9 |
10 | ## AroCrypt Исходный Код / _Read in [English](README.md)_
11 | AroCrypt это современный кроссплатформенный инструмент шифрования для надежной защиты данных без лишней сложности. Шифруйте текст, файлы и изображения с помощью проверенных криптографических алгоритмов. Интерфейс быстрый, чистый и удобный для разработчиков, а безопасность непробиваемая. Независимо от того, защищаете ли вы личные заметки или создаете безопасные рабочие процессы, AroCrypt обеспечивает надежную защиту без лишней сложности.
12 |
13 |
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 |
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 |
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}}1> 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}}1> Алгоритм",
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 |
--------------------------------------------------------------------------------