├── .eslintignore ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── src ├── renderer │ ├── src │ │ ├── assets │ │ │ └── main.css │ │ ├── main.jsx │ │ ├── icons │ │ │ ├── eyeFilledIcon.jsx │ │ │ └── eyeSlashFilledIcon.jsx │ │ ├── components │ │ │ └── fileInput.jsx │ │ └── App.jsx │ └── index.html ├── preload │ └── index.js └── main │ └── index.js ├── assets └── icons │ ├── mac │ └── logo.icns │ ├── win │ └── icon.ico │ └── linux │ └── logo.png ├── .prettierignore ├── postcss.config.js ├── .prettierrc ├── .eslintrc.cjs ├── electron.vite.config.mjs ├── tailwind.config.js ├── LICENSE ├── electron-builder.yml ├── package.json ├── README.md └── lib └── crypt └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | *.log* 6 | .env 7 | build 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /assets/icons/mac/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truethari/Cryptique/HEAD/assets/icons/mac/logo.icns -------------------------------------------------------------------------------- /assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truethari/Cryptique/HEAD/assets/icons/win/icon.ico -------------------------------------------------------------------------------- /assets/icons/linux/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/truethari/Cryptique/HEAD/assets/icons/linux/logo.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | node_modules 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": false, 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:react/recommended', 5 | 'plugin:react/jsx-runtime', 6 | '@electron-toolkit', 7 | '@electron-toolkit/eslint-config-prettier' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/src/main.jsx: -------------------------------------------------------------------------------- 1 | import "./assets/main.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | import { NextUIProvider } from "@nextui-org/react"; 6 | import App from "./App"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")).render( 9 | 10 | 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import react from '@vitejs/plugin-react' 4 | 5 | export default defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src') 16 | } 17 | }, 18 | plugins: [react()] 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cryptique 6 | 7 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/preload/index.js: -------------------------------------------------------------------------------- 1 | import { contextBridge } from "electron"; 2 | import { electronAPI } from "@electron-toolkit/preload"; 3 | 4 | import { encrypt, decrypt } from "../../lib/crypt"; 5 | 6 | const api = { 7 | encrypt, 8 | decrypt, 9 | }; 10 | 11 | if (process.contextIsolated) { 12 | try { 13 | contextBridge.exposeInMainWorld("electron", electronAPI); 14 | contextBridge.exposeInMainWorld("api", api); 15 | } catch (error) { 16 | console.error(error); 17 | } 18 | } else { 19 | window.electron = electronAPI; 20 | window.api = api; 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("tailwindcss").Config} */ 2 | const { nextui } = require("@nextui-org/react"); 3 | 4 | module.exports = { 5 | content: [ 6 | "./src/renderer/index.html", 7 | "./src/renderer/src/**/*.{svelte,js,ts,jsx,tsx}", 8 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", 9 | ], 10 | 11 | theme: { 12 | fontFamily: { 13 | sans: ["Inter", "sans-serif"], 14 | code: ["Fira Code", "monospace"], 15 | }, 16 | extend: {}, 17 | }, 18 | 19 | darkMode: "class", 20 | plugins: [nextui()], 21 | }; 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tharindu N. Madhusanka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/renderer/src/icons/eyeFilledIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const EyeFilledIcon = (props) => ( 4 | 23 | ); 24 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: cryptique 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | asarUnpack: 12 | - resources/** 13 | win: 14 | executableName: cryptique 15 | nsis: 16 | artifactName: ${name}-${version}-setup.${ext} 17 | shortcutName: ${productName} 18 | uninstallDisplayName: ${productName} 19 | createDesktopShortcut: always 20 | mac: 21 | entitlementsInherit: build/entitlements.mac.plist 22 | extendInfo: 23 | - NSCameraUsageDescription: Application requests access to the device's camera. 24 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 25 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 26 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 27 | notarize: false 28 | dmg: 29 | artifactName: ${name}-${version}.${ext} 30 | linux: 31 | target: 32 | - AppImage 33 | - snap 34 | - deb 35 | maintainer: electronjs.org 36 | category: Utility 37 | appImage: 38 | artifactName: ${name}-${version}.${ext} 39 | npmRebuild: false 40 | publish: 41 | provider: generic 42 | url: https://example.com/auto-updates 43 | -------------------------------------------------------------------------------- /src/renderer/src/components/fileInput.jsx: -------------------------------------------------------------------------------- 1 | export default function FileInput({ setFile }) { 2 | return ( 3 | <> 4 |
5 | 35 |
36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cryptique", 3 | "version": "1.0.0", 4 | "description": "A simple and secure file encryption tool", 5 | "main": "./out/main/index.js", 6 | "author": "tharindu.dev", 7 | "homepage": "https://tharindu.dev", 8 | "scripts": { 9 | "format": "prettier --write .", 10 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 11 | "start": "electron-vite preview", 12 | "dev": "electron-vite dev", 13 | "build": "electron-vite build", 14 | "postinstall": "electron-builder install-app-deps", 15 | "build:unpack": "npm run build && electron-builder --dir", 16 | "build:win": "npm run build && electron-builder --win", 17 | "build:mac": "npm run build && electron-builder --mac", 18 | "build:linux": "npm run build && electron-builder --linux" 19 | }, 20 | "build": { 21 | "appId": "dev.tharindu.cryptique", 22 | "productName": "Cryptique", 23 | "directories": { 24 | "output": "dist" 25 | }, 26 | "mac": { 27 | "icon": "assets/icons/mac/logo.icns" 28 | }, 29 | "linux": { 30 | "icon": "assets/icons/png/logo.png" 31 | }, 32 | "win": { 33 | "icon": "assets/icons/win/logo.ico" 34 | } 35 | }, 36 | "dependencies": { 37 | "@electron-toolkit/preload": "^3.0.0", 38 | "@electron-toolkit/utils": "^3.0.0", 39 | "@nextui-org/react": "^2.2.10", 40 | "framer-motion": "^11.0.28", 41 | "react-hot-toast": "^2.4.1" 42 | }, 43 | "devDependencies": { 44 | "@electron-toolkit/eslint-config": "^1.0.2", 45 | "@electron-toolkit/eslint-config-prettier": "^2.0.0", 46 | "@vitejs/plugin-react": "^4.2.1", 47 | "autoprefixer": "^10.4.19", 48 | "electron": "^28.2.0", 49 | "electron-builder": "^24.9.1", 50 | "electron-vite": "^2.0.0", 51 | "eslint": "^8.56.0", 52 | "eslint-plugin-react": "^7.33.2", 53 | "postcss": "^8.4.38", 54 | "postcss-loader": "^8.1.1", 55 | "prettier": "^3.2.4", 56 | "react": "^18.2.0", 57 | "react-dom": "^18.2.0", 58 | "tailwindcss": "^3.4.3", 59 | "vite": "^5.0.12" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/src/icons/eyeSlashFilledIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const EyeSlashFilledIcon = (props) => ( 4 | 35 | ); 36 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { app, shell, BrowserWindow, ipcMain, Menu, MenuItem } from "electron"; 2 | import { join } from "path"; 3 | import { electronApp, optimizer, is } from "@electron-toolkit/utils"; 4 | 5 | function createWindow() { 6 | const mainWindow = new BrowserWindow({ 7 | width: 800, 8 | height: 670, 9 | show: false, 10 | resizable: false, 11 | autoHideMenuBar: true, 12 | ...(process.platform === "linux" ? {} : {}), 13 | webPreferences: { 14 | preload: join(__dirname, "../preload/index.js"), 15 | contextIsolation: true, 16 | sandbox: false, 17 | }, 18 | icon: "assets/icons/linux/icon.ico", 19 | }); 20 | 21 | mainWindow.setMenuBarVisibility(false); 22 | 23 | mainWindow.on("ready-to-show", () => { 24 | mainWindow.show(); 25 | }); 26 | 27 | mainWindow.webContents.setWindowOpenHandler((details) => { 28 | shell.openExternal(details.url); 29 | return { action: "deny" }; 30 | }); 31 | 32 | // HMR for renderer base on electron-vite cli. 33 | // Load the remote URL for development or the local html file for production. 34 | if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { 35 | mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]); 36 | } else { 37 | mainWindow.loadFile(join(__dirname, "../renderer/index.html")); 38 | } 39 | } 40 | 41 | const template = [ 42 | { 43 | label: "Help", 44 | submenu: [ 45 | { 46 | role: "About", 47 | }, 48 | ], 49 | }, 50 | ]; 51 | 52 | const menu = Menu.buildFromTemplate(template); 53 | Menu.setApplicationMenu(menu); 54 | 55 | // This method will be called when Electron has finished 56 | // initialization and is ready to create browser windows. 57 | // Some APIs can only be used after this event occurs. 58 | app.whenReady().then(() => { 59 | // Set app user model id for windows 60 | electronApp.setAppUserModelId("com.electron"); 61 | 62 | // Default open or close DevTools by F12 in development 63 | // and ignore CommandOrControl + R in production. 64 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 65 | app.on("browser-window-created", (_, window) => { 66 | optimizer.watchWindowShortcuts(window); 67 | }); 68 | 69 | // IPC test 70 | ipcMain.on("ping", () => console.log("pong")); 71 | 72 | createWindow(); 73 | 74 | app.on("activate", function () { 75 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 76 | }); 77 | }); 78 | 79 | app.on("window-all-closed", () => { 80 | if (process.platform !== "darwin") { 81 | app.quit(); 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptique 2 | 3 | --- 4 | 5 | cryptique logo 6 | 7 | cryptique v1 8 | 9 | --- 10 | 11 | ## Table of Contents 12 | 13 | - [Introduction](#introduction) 14 | - [Features](#-features) 15 | - [Prerequisites](#prerequisites) 16 | - [Installation](#-installation) 17 | - [Usage](#-usage) 18 | - [License](#-license) 19 | - [Contributing](#-contributing) 20 | 21 | --- 22 | 23 | ## Introduction 24 | 25 | Cryptique is a secure file encryption and decryption tool built with Electron and Node.js. It leverages the cryptographic functionality provided by Node.js's `crypto` module to perform AES-256-CBC encryption and decryption of files, ensuring that your sensitive data remains protected. 26 | 27 | ## 📚 Features 28 | 29 | - **File Encryption**: Securely encrypts files using AES-256-CBC and more algorithms, ensuring that data cannot be accessed without the correct password. 30 | - **File Decryption**: Decrypts previously encrypted files with the same password used for encryption. 31 | - **Progress Reporting**: Displays encryption and decryption progress, updating in real-time. 32 | - **Error Handling**: Comprehensive error reporting to help diagnose issues during the encryption or decryption processes. 33 | 34 | ## Prerequisites 35 | 36 | Before you begin, ensure you have the following installed on your system: 37 | 38 | - Node.js (v12.0.0 or higher) 39 | - npm (v6.0.0 or higher) 40 | - Electron 41 | 42 | ## 🛠 Installation 43 | 44 | Clone the repository to your local machine: 45 | 46 | ```bash 47 | git clone https://github.com/truethari/cryptoque.git 48 | cd cryptoque 49 | ``` 50 | 51 | ```bash 52 | $ npm install 53 | ``` 54 | 55 | ### Development 56 | 57 | ```bash 58 | $ npm run dev 59 | ``` 60 | 61 | ### Build 62 | 63 | ```bash 64 | # For windows 65 | $ npm run build:win 66 | 67 | # For macOS 68 | $ npm run build:mac 69 | 70 | # For Linux 71 | $ npm run build:linux 72 | ``` 73 | 74 | ## 🚀 Usage 75 | 76 | ### Encrypting a File 77 | 78 | - Open the application. 79 | - Switch to the "Encrypt" tab. 80 | - Select a file using the file picker. 81 | - Enter a secure password in the password field. 82 | - Click "Encrypt File" to begin the encryption process. 83 | - The encrypted file will be saved in the same directory as the original file with a .enc extension. 84 | 85 | ### Decrypting a File 86 | 87 | - Open the application. 88 | - Switch to the "Decrypt" tab. 89 | - Select an encrypted file (.enc) using the file picker. 90 | - Enter the password used during encryption. 91 | - Click "Decrypt File" to begin the decryption process. 92 | - The decrypted file will be restored under the same directory as the encrypted file. 93 | 94 | ## 📄 License 95 | 96 | Distributed under the MIT License. See [LICENSE](https://github.com/truethari/Cryptique/blob/master/LICENSE) for more information. 97 | 98 | ## 🌱 Contribution 99 | 100 | If you have any suggestions on what to improve in Reactfolio and would like to share them, feel free to leave an issue or fork project to implement your own ideas 101 | -------------------------------------------------------------------------------- /src/renderer/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import toast, { Toaster } from "react-hot-toast"; 3 | import { Input, Button } from "@nextui-org/react"; 4 | import { Tabs, Tab } from "@nextui-org/react"; 5 | import { Select, SelectItem } from "@nextui-org/react"; 6 | import { Progress } from "@nextui-org/react"; 7 | 8 | import FileInput from "./components/fileInput"; 9 | 10 | import { EyeFilledIcon } from "./icons/eyeFilledIcon"; 11 | import { EyeSlashFilledIcon } from "./icons/eyeSlashFilledIcon"; 12 | 13 | const _algorithms = ["aes-256-cbc", "aes-256-cfb", "aes-256-ctr", "aes-256-gcm", "aes-256-ofb"]; 14 | 15 | const algorithms = _algorithms.map((algorithm) => ({ 16 | label: algorithm, 17 | value: algorithm, 18 | })); 19 | 20 | export default function App() { 21 | const [activeTab, setActiveTab] = useState("encrypt"); 22 | const [password, setPassword] = useState(""); 23 | const [isVisible, setIsVisible] = useState(false); 24 | const toggleVisibility = () => setIsVisible(!isVisible); 25 | 26 | const [selectedAlgorithm, setSelectedAlgorithm] = useState(new Set(["aes-256-cbc"])); 27 | 28 | const [progress, setProgress] = useState(0); 29 | const [file, setFile] = useState(null); 30 | const [isSuccess, setIsSucess] = useState(false); 31 | const [isError, setIsError] = useState(false); 32 | 33 | useEffect(() => { 34 | setProgress(0); 35 | setIsSucess(false); 36 | setIsError(false); 37 | }, [activeTab]); 38 | 39 | function updateProgress(progress) { 40 | setProgress(progress); 41 | } 42 | 43 | const errorToast = (icon, message) => 44 | toast(message, { 45 | icon: `${icon}`, 46 | 47 | ariaProps: { 48 | role: "status", 49 | "aria-live": "polite", 50 | }, 51 | 52 | style: { 53 | background: "red", 54 | color: "white", 55 | }, 56 | }); 57 | 58 | function updateError(error) { 59 | console.log("error", error); 60 | setIsError(true); 61 | 62 | toast.custom( 63 |
64 |
65 | ✋ There was an error processing the file. 66 | Reasons could be: 67 | 68 | 74 |
75 |
, 76 | ); 77 | } 78 | 79 | const handleButton = async () => { 80 | if (!password || password.length === 0) return errorToast("🔑", "Please enter a password"); 81 | 82 | const selectedAlgorithms = Array.from(selectedAlgorithm); 83 | if (selectedAlgorithms.length === 0) return errorToast("🔒", "Please select an algorithm"); 84 | const _algo = selectedAlgorithms[0]; 85 | 86 | console.log("algo", _algo); 87 | 88 | setIsSucess(false); 89 | setIsError(false); 90 | const filepath = file.path; 91 | try { 92 | if (activeTab === "encrypt") { 93 | await window.api.encrypt(filepath, password, _algo, updateProgress, updateError); 94 | } else { 95 | await window.api.decrypt(filepath, password, _algo, updateProgress, updateError); 96 | } 97 | setIsSucess(true); 98 | } catch (error) { 99 | console.log("errr", error); 100 | } 101 | }; 102 | 103 | return ( 104 | <> 105 | 106 | 107 |
108 |
109 |

Cryptoque

110 | 111 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | {file && ( 125 | <> 126 |
127 |
File name:
128 |
{file.name}
129 | 130 |
File size:
131 |
{file.size} bytes
132 | 133 |
File type:
134 |
{file.type}
135 |
136 | 137 | )} 138 | 139 |
140 | 148 | {isVisible ? ( 149 | 150 | ) : ( 151 | 152 | )} 153 | 154 | } 155 | /> 156 | 157 | 173 | 174 | 182 |
183 | 184 |
185 | {progress > 0 && !isError && ( 186 | 194 | )} 195 | 196 | {isSuccess && ( 197 | File {activeTab === "encrypt" ? "encrypted" : "decrypted"} successfully! 198 | )} 199 |
200 |
201 |
202 | 203 | ); 204 | } 205 | -------------------------------------------------------------------------------- /lib/crypt/index.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | const fsp = require("fs").promises; 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | export const encrypt = async ( 7 | sourceFilePath, 8 | password, 9 | algorithm, 10 | progressCallback, 11 | errorCallback, 12 | ) => { 13 | const destFilePath = path.join(sourceFilePath + ".enc"); 14 | 15 | try { 16 | // Attempt to delete the destination file if it exists 17 | if (fs.existsSync(destFilePath)) { 18 | await fsp.unlink(destFilePath); 19 | } 20 | } catch (err) { 21 | const errorMessage = "Error deleting existing encrypted file: " + err.message; 22 | console.error(errorMessage); 23 | errorCallback(errorMessage); 24 | return; // Stop execution if we cannot delete existing file 25 | } 26 | 27 | try { 28 | const salt = crypto.randomBytes(16); 29 | const key = crypto.scryptSync(password, salt, 32); 30 | const iv = crypto.randomBytes(16); 31 | const cipher = crypto.createCipheriv(algorithm, key, iv); 32 | const readStream = fs.createReadStream(sourceFilePath); 33 | const writeStream = fs.createWriteStream(destFilePath); 34 | const header = Buffer.concat([salt, iv]); 35 | 36 | const fileStats = await fsp.stat(sourceFilePath); 37 | const totalSize = fileStats.size; 38 | let encryptedSize = 0; 39 | 40 | return new Promise((resolve, reject) => { 41 | writeStream.on("open", () => { 42 | writeStream.write(header); 43 | 44 | readStream.on("data", (chunk) => { 45 | encryptedSize += chunk.length; 46 | const progress = ((encryptedSize / totalSize) * 100).toFixed(2); 47 | progressCallback(progress); 48 | }); 49 | 50 | cipher.on("error", (err) => { 51 | console.error("Cipher stream error:", err); 52 | readStream.destroy(); // Stop reading 53 | writeStream.end(); // Close write stream 54 | const errorMessage = `Cipher operation failed: ${err.message}`; 55 | errorCallback(errorMessage); 56 | reject(new Error(errorMessage)); 57 | }); 58 | 59 | writeStream.on("error", (err) => { 60 | console.error("Write stream error:", err); 61 | readStream.destroy(); // Stop reading 62 | cipher.end(); // End cipher operation 63 | const errorMessage = `Write to disk failed: ${err.message}`; 64 | errorCallback(errorMessage); 65 | reject(new Error(errorMessage)); 66 | }); 67 | 68 | readStream 69 | .pipe(cipher) 70 | .pipe(writeStream) 71 | .on("finish", () => { 72 | console.log(`File has been encrypted and saved as ${destFilePath}`); 73 | resolve(destFilePath); 74 | }) 75 | .on("error", (err) => { 76 | console.error("Piping streams failed:", err); 77 | const errorMessage = `Stream piping failed: ${err.message}`; 78 | errorCallback(errorMessage); 79 | reject(new Error(errorMessage)); 80 | }); 81 | }); 82 | }); 83 | } catch (err) { 84 | const errorMessage = "Encryption setup failed due to: " + err.message; 85 | console.error(errorMessage); 86 | errorCallback(errorMessage); 87 | return Promise.reject(new Error(errorMessage)); 88 | } 89 | }; 90 | 91 | export const decrypt = async ( 92 | sourceFilePath, 93 | password, 94 | algorithm, 95 | progressCallback, 96 | errorCallback, 97 | ) => { 98 | const destFilePath = path.join( 99 | path.dirname(sourceFilePath), 100 | `decrypted-${path.basename(sourceFilePath).replace(".enc", "")}`, 101 | ); 102 | 103 | try { 104 | if (fs.existsSync(destFilePath)) { 105 | await fsp.unlink(destFilePath); 106 | } 107 | } catch (err) { 108 | if (err.code !== "ENOENT") { 109 | const errorMessage = "Error deleting existing decrypted file: " + err.message; 110 | console.error(errorMessage); 111 | errorCallback(errorMessage); 112 | return; 113 | } 114 | } 115 | 116 | let readStream; 117 | let writeStream; 118 | 119 | try { 120 | readStream = fs.createReadStream(sourceFilePath); 121 | writeStream = fs.createWriteStream(destFilePath); 122 | } catch (err) { 123 | const errorMessage = `Failed to create read/write streams: ${err.message}`; 124 | console.error(errorMessage); 125 | errorCallback(errorMessage); 126 | return Promise.reject(new Error(errorMessage)); 127 | } 128 | 129 | return new Promise((resolve, reject) => { 130 | readStream.once("readable", () => { 131 | const header = readStream.read(32); 132 | if (!header) { 133 | const errorMessage = "Failed to read header from encrypted file."; 134 | console.error(errorMessage); 135 | reject(new Error(errorMessage)); 136 | errorCallback(errorMessage); 137 | return; 138 | } 139 | 140 | let decipher; 141 | 142 | try { 143 | const salt = header.slice(0, 16); 144 | const iv = header.slice(16, 32); 145 | const key = crypto.scryptSync(password, salt, 32); 146 | decipher = crypto.createDecipheriv(algorithm, key, iv); 147 | } catch (err) { 148 | const errorMessage = `Failed to create decipher: ${err.message}`; 149 | console.error(errorMessage); 150 | errorCallback(errorMessage); 151 | reject(new Error(errorMessage)); 152 | return; 153 | } 154 | 155 | let fileStats; 156 | try { 157 | fileStats = fs.statSync(sourceFilePath); 158 | } catch (err) { 159 | const errorMessage = `Failed to stat file: ${err.message}`; 160 | console.error(errorMessage); 161 | errorCallback(errorMessage); 162 | reject(new Error(errorMessage)); 163 | return; 164 | } 165 | 166 | const totalSize = fileStats.size - header.length; 167 | let decryptedSize = 0; 168 | 169 | readStream.on("data", (chunk) => { 170 | decryptedSize += chunk.length; 171 | const progress = ((decryptedSize / totalSize) * 100).toFixed(2); 172 | progressCallback(progress); 173 | }); 174 | 175 | decipher.on("error", (err) => { 176 | console.log("Decipher stream error", err); 177 | readStream.destroy(); // Prevent further reading 178 | writeStream.end(); // Prevent further writing 179 | const errorMessage = `Deciphering failed: ${err.message}`; 180 | errorCallback(errorMessage); 181 | reject(new Error(errorMessage)); 182 | }); 183 | 184 | writeStream.on("error", (err) => { 185 | console.log("Write stream error", err); 186 | readStream.destroy(); // Prevent further reading 187 | decipher.end(); // Prevent further deciphering 188 | const errorMessage = `Write to disk failed: ${err.message}`; 189 | errorCallback(errorMessage); 190 | reject(new Error(errorMessage)); 191 | }); 192 | 193 | readStream 194 | .pipe(decipher) 195 | .pipe(writeStream) 196 | .on("finish", () => { 197 | console.log(`File has been decrypted and saved as ${destFilePath}`); 198 | resolve(destFilePath); 199 | }) 200 | .on("error", (err) => { 201 | const errorMessage = `Stream piping failed: ${err.message}`; 202 | errorCallback(errorMessage); 203 | reject(new Error(errorMessage)); 204 | }); 205 | }); 206 | }); 207 | }; 208 | --------------------------------------------------------------------------------