├── .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 |
6 |
7 |
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 |
69 | - Incorrect password
70 | - File is not encrypted
71 | - File is corrupted
72 | - Algorithm mismatch
73 |
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 |
--------------------------------------------------------------------------------