diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index f8c40cc..1d0b66f 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,8 +4,15 @@
-
-
+
+
+
+
+
+
+
+
+
@@ -22,7 +29,7 @@
@@ -199,56 +206,57 @@
- {
- "keyToString": {
- "ASKED_ADD_EXTERNAL_FILES": "true",
- "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
- "Docker.Dockerfile build.executor": "Run",
- "Docker.Dockerfile.executor": "Run",
- "Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
- "Playwright.JoinText Component.executor": "Run",
- "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
- "RunOnceActivity.OpenProjectViewOnStart": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.git.unshallow": "true",
- "Vitest.compute function (1).executor": "Run",
- "Vitest.compute function.executor": "Run",
- "Vitest.mergeText.executor": "Run",
- "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
- "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
- "Vitest.parsePageRanges.executor": "Run",
- "Vitest.removeDuplicateLines function.executor": "Run",
- "Vitest.removeDuplicateLines function.newlines option.executor": "Run",
- "Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
- "Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
- "Vitest.replaceText function.executor": "Run",
- "Vitest.timeBetweenDates.executor": "Run",
- "git-widget-placeholder": "main",
- "ignore.virus.scanning.warn.message": "true",
- "kotlin-language-version-configured": "true",
- "last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/pages/tools/json",
- "node.js.detected.package.eslint": "true",
- "node.js.detected.package.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_package_manager_path": "npm",
- "npm.build.executor": "Run",
- "npm.dev.executor": "Run",
- "npm.lint.executor": "Run",
- "npm.prebuild.executor": "Run",
- "npm.script:create:tool.executor": "Run",
- "npm.test.executor": "Run",
- "npm.test:e2e.executor": "Run",
- "npm.test:e2e:run.executor": "Run",
- "prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
- "project.structure.last.edited": "Problems",
- "project.structure.proportion": "0.0",
- "project.structure.side.proportion": "0.2",
- "settings.editor.selected.configurable": "refactai_advanced_settings",
- "ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
- "vue.rearranger.settings.migration": "true"
+
+}]]>
+
-
@@ -353,11 +361,11 @@
+
-
@@ -462,14 +470,12 @@
-
-
-
- 1741492688761
-
-
-
- 1741492688761
+
+
+
+
+
+
@@ -855,7 +861,15 @@
1749147227565
-
+
+
+ 1749348977357
+
+
+
+ 1749348977357
+
+
@@ -902,8 +916,6 @@
-
-
@@ -927,7 +939,9 @@
-
+
+
+
diff --git a/src/lib/ghostscript/background-worker.js b/src/lib/ghostscript/background-worker.js
index 3bee269..251d430 100644
--- a/src/lib/ghostscript/background-worker.js
+++ b/src/lib/ghostscript/background-worker.js
@@ -1,4 +1,4 @@
-import { COMPRESS_ACTION, PROTECT_ACTION } from './worker-init';
+import { COMPRESS_ACTION, PROTECT_ACTION, UNLOCK_ACTION } from './worker-init';
function loadScript() {
import('./gs-worker.js');
@@ -87,7 +87,7 @@ function protectPdf(dataStruct, responseCallback) {
// Validate password
if (!password) {
- responseCallback({
+ console.error({
error: 'Password is required for encryption',
url: dataStruct.url
});
@@ -153,6 +153,178 @@ function protectPdf(dataStruct, responseCallback) {
xhr.send();
}
+function unlockPdf(dataStruct, responseCallback) {
+ const speed = dataStruct.speed || 'normal';
+ let passwordListUrl;
+ console.log('unlockPdf', dataStruct);
+ // Determine which password list to download based on speed
+ if (speed === 'fast') {
+ passwordListUrl =
+ 'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Common-Credentials/xato-net-10-million-passwords-1000.txt';
+ } else if (speed === 'normal') {
+ passwordListUrl =
+ 'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Common-Credentials/xato-net-10-million-passwords-100000.txt';
+ } else {
+ // Default or handle other speeds
+ passwordListUrl =
+ 'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Common-Credentials/xato-net-10-million-passwords-100000.txt';
+ }
+
+ // --- Step 1: Download the password list ---
+ var xhrPasswordList = new XMLHttpRequest();
+ xhrPasswordList.open('GET', passwordListUrl);
+ xhrPasswordList.onload = function () {
+ if (xhrPasswordList.status === 200) {
+ const passwordList = xhrPasswordList.responseText
+ .split('\n')
+ .map((p) => p.trim());
+
+ // --- Step 2: Proceed with PDF processing and dictionary attack ---
+ processPdfWithDictionary(dataStruct, responseCallback, passwordList);
+ } else {
+ console.error(
+ 'Failed to download password list:',
+ xhrPasswordList.statusText
+ );
+ console.error('Failed to download password list');
+ }
+ };
+ xhrPasswordList.onerror = function () {
+ console.error('Network error while downloading password list.');
+ console.error('Network error while downloading password list');
+ };
+ xhrPasswordList.send();
+}
+
+function processPdfWithDictionary(dataStruct, responseCallback, passwordList) {
+ var xhrPdf = new XMLHttpRequest();
+ xhrPdf.open('GET', dataStruct.psDataURL);
+ xhrPdf.responseType = 'arraybuffer';
+ xhrPdf.onload = function () {
+ console.log('PDF onload');
+ self.URL.revokeObjectURL(dataStruct.psDataURL);
+
+ let currentPasswordIndex = 0;
+ const tryNextPassword = () => {
+ if (currentPasswordIndex >= passwordList.length) {
+ console.error('All passwords tried. PDF could not be unlocked.');
+ return;
+ }
+
+ const password = passwordList[currentPasswordIndex];
+ console.log(
+ `Attempting with password: "${password}" (${currentPasswordIndex + 1}/${
+ passwordList.length
+ })`
+ );
+
+ Module = {
+ preRun: [
+ function () {
+ self.Module.FS.writeFile(
+ 'input.pdf',
+ new Uint8Array(xhrPdf.response)
+ );
+ }
+ ],
+ postRun: [
+ function () {
+ try {
+ var uarray = self.Module.FS.readFile('output.pdf', {
+ encoding: 'binary'
+ });
+ // If readFile succeeds, it means the password was correct
+ var blob = new Blob([uarray], {
+ type: 'application/octet-stream'
+ });
+ var pdfDataURL = self.URL.createObjectURL(blob);
+ console.log(
+ `PDF unlocked successfully with password: "${password}"`
+ );
+ responseCallback({
+ pdfDataURL: pdfDataURL,
+ url: dataStruct.url,
+ type: PROTECT_ACTION
+ });
+ } catch (error) {
+ console.error('Error:', error);
+ }
+ }
+ ],
+ arguments: [
+ '-sDEVICE=pdfwrite',
+ '-dCompatibilityLevel=1.4',
+ `-sPDFPassword=${password}`, // Use the current password from the list
+ '-DNOPAUSE',
+ '-dQUIET',
+ '-dBATCH',
+ '-sOutputFile=output.pdf',
+ 'input.pdf'
+ ],
+ print: function (text) {
+ if (text.includes('Error: Password did not work')) {
+ try {
+ if (self.Module && typeof self.Module.exit === 'function') {
+ self.Module.exit(1);
+ }
+ // Clean up any files that might have been created
+ if (self.Module && self.Module.FS) {
+ try {
+ if (self.Module.FS.readdir('/').includes('input.pdf')) {
+ self.Module.FS.unlink('input.pdf');
+ }
+ if (self.Module.FS.readdir('/').includes('output.pdf')) {
+ self.Module.FS.unlink('output.pdf');
+ }
+ } catch (fsError) {
+ console.log('Filesystem cleanup error:', fsError);
+ }
+ }
+ } catch (exitError) {
+ console.log('Error during module exit:', exitError);
+ }
+
+ currentPasswordIndex++;
+ setTimeout(tryNextPassword, 10); // Small delay to prevent tight loop
+ }
+ },
+ printErr: function (text) {
+ // Ghostscript might print errors here if the password is wrong
+ // We need to parse these to determine if it's a password error or another issue
+ console.error('Ghostscript error:', text);
+ },
+ totalDependencies: 0,
+ noExitRuntime: 1
+ };
+
+ if (!self.Module) {
+ self.Module = Module;
+ loadScript(); // Assuming loadScript() loads the Emscripten-compiled Ghostscript
+ } else {
+ self.Module['calledRun'] = false;
+ self.Module['postRun'] = Module.postRun;
+ self.Module['preRun'] = Module.preRun;
+ self.Module['arguments'] = Module.arguments;
+ self.Module.callMain();
+ }
+ };
+
+ // Start the dictionary attack
+ tryNextPassword();
+ };
+
+ xhrPdf.onerror = function () {
+ console.error('Network error while downloading PDF.');
+ };
+ xhrPdf.send();
+}
+
+// Assuming PROTECT_ACTION and loadScript() are defined elsewhere in your worker/global scope.
+// For example:
+// const PROTECT_ACTION = 'pdf_protection_status';
+// function loadScript() {
+// importScripts('ghostscript_module.js'); // Or whatever your Emscripten output file is named
+// }
self.addEventListener('message', function ({ data: e }) {
console.log('message', e);
// e.data contains the message sent to the worker.
@@ -160,13 +332,16 @@ self.addEventListener('message', function ({ data: e }) {
return;
}
console.log('Message received from main script', e.data);
- const responseCallback = ({ pdfDataURL, type }) => {
+ const responseCallback = ({ pdfDataURL, type, log }) => {
self.postMessage(pdfDataURL);
+ console.log(log);
};
if (e.data.type === COMPRESS_ACTION) {
compressPdf(e.data, responseCallback);
} else if (e.data.type === PROTECT_ACTION) {
protectPdf(e.data, responseCallback);
+ } else if (e.data.type === UNLOCK_ACTION) {
+ unlockPdf(e.data, responseCallback);
}
});
diff --git a/src/lib/ghostscript/worker-init.ts b/src/lib/ghostscript/worker-init.ts
index 9fad88f..88cc6ab 100644
--- a/src/lib/ghostscript/worker-init.ts
+++ b/src/lib/ghostscript/worker-init.ts
@@ -1,5 +1,6 @@
export const COMPRESS_ACTION = 'compress-pdf';
export const PROTECT_ACTION = 'protect-pdf';
+export const UNLOCK_ACTION = 'unlock-pdf';
export async function compressWithGhostScript(dataStruct: {
psDataURL: string;
@@ -14,6 +15,7 @@ export async function compressWithGhostScript(dataStruct: {
export async function protectWithGhostScript(dataStruct: {
psDataURL: string;
+ password: string;
}): Promise {
const worker = getWorker();
worker.postMessage({
@@ -23,6 +25,18 @@ export async function protectWithGhostScript(dataStruct: {
return getListener(worker);
}
+export async function unlockWithGhostScript(dataStruct: {
+ psDataURL: string;
+ speed: 'slow' | 'normal' | 'fast';
+}): Promise {
+ const worker = getWorker();
+ worker.postMessage({
+ data: { ...dataStruct, type: UNLOCK_ACTION },
+ target: 'wasm'
+ });
+ return getListener(worker);
+}
+
const getListener = (worker: Worker): Promise => {
return new Promise((resolve, reject) => {
const listener = (e: MessageEvent) => {
diff --git a/src/pages/tools/pdf/index.ts b/src/pages/tools/pdf/index.ts
index c5f294f..ececd28 100644
--- a/src/pages/tools/pdf/index.ts
+++ b/src/pages/tools/pdf/index.ts
@@ -5,6 +5,7 @@ import { DefinedTool } from '@tools/defineTool';
import { tool as compressPdfTool } from './compress-pdf/meta';
import { tool as protectPdfTool } from './protect-pdf/meta';
import { meta as pdfToEpub } from './pdf-to-epub/meta';
+import { meta as unlockPdfTool } from './unlock-pdf/meta';
export const pdfTools: DefinedTool[] = [
splitPdfMeta,
@@ -12,5 +13,6 @@ export const pdfTools: DefinedTool[] = [
compressPdfTool,
protectPdfTool,
mergePdf,
- pdfToEpub
+ pdfToEpub,
+ unlockPdfTool
];
diff --git a/src/pages/tools/pdf/unlock-pdf/index.tsx b/src/pages/tools/pdf/unlock-pdf/index.tsx
new file mode 100644
index 0000000..af5f73f
--- /dev/null
+++ b/src/pages/tools/pdf/unlock-pdf/index.tsx
@@ -0,0 +1,109 @@
+import React, { useContext, useState } from 'react';
+import ToolContent from '@components/ToolContent';
+import { ToolComponentProps } from '@tools/defineTool';
+import ToolPdfInput from '@components/input/ToolPdfInput';
+import ToolFileResult from '@components/result/ToolFileResult';
+import { InitialValuesType, Speed } from './types';
+import { unlockPdf } from './service';
+import { CustomSnackBarContext } from '../../../../contexts/CustomSnackBarContext';
+import RadioWithTextField from '@components/options/RadioWithTextField';
+import SimpleRadio from '@components/options/SimpleRadio';
+
+const initialValues: InitialValuesType = {
+ speed: 'normal'
+};
+
+const speeds: {
+ label: string;
+ description: string;
+ value: Speed;
+}[] = [
+ {
+ label: 'Slow',
+ value: 'slow',
+ description: 'More probable to unlock the PDF, but may take longer.'
+ },
+ {
+ label: 'Normal',
+ value: 'normal',
+ description: 'Balanced between speed and success rate.'
+ },
+ {
+ label: 'Fast',
+ value: 'fast',
+ description:
+ 'Faster unlocking, but less thorough; may fail on complex passwords.'
+ }
+];
+
+export default function UnlockPdf({
+ title,
+ longDescription
+}: ToolComponentProps) {
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const { showSnackBar } = useContext(CustomSnackBarContext);
+
+ const compute = async (values: InitialValuesType, input: File | null) => {
+ if (!input) return;
+
+ try {
+ setIsProcessing(true);
+ const protectedPdf = await unlockPdf(input, values);
+ setResult(protectedPdf);
+ } catch (error) {
+ console.error('Error protecting PDF:', error);
+ showSnackBar(
+ `Failed to protect PDF: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ 'error'
+ );
+ setResult(null);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ return (
+
+ }
+ resultComponent={
+
+ }
+ getGroups={({ values, updateField }) => [
+ {
+ title: 'Speed',
+ component: speeds.map(({ label, description, value }) => (
+ updateField('speed', value)}
+ />
+ ))
+ }
+ ]}
+ />
+ );
+}
diff --git a/src/pages/tools/pdf/unlock-pdf/meta.ts b/src/pages/tools/pdf/unlock-pdf/meta.ts
new file mode 100644
index 0000000..5168a64
--- /dev/null
+++ b/src/pages/tools/pdf/unlock-pdf/meta.ts
@@ -0,0 +1,24 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const meta = defineTool('pdf', {
+ name: 'Unlock PDF',
+ path: 'unlock-pdf',
+ icon: 'material-symbols:lock_open',
+ description:
+ 'Remove password protection from your PDF files securely in your browser',
+ shortDescription: 'Unlock PDF files securely',
+ keywords: [
+ 'pdf',
+ 'unlock',
+ 'remove password',
+ 'decrypt',
+ 'unprotect',
+ 'security',
+ 'browser',
+ 'decryption'
+ ],
+ longDescription:
+ 'Remove password protection from your PDF files securely in your browser. Your files never leave your device, ensuring complete privacy while unlocking your documents for easier access and sharing.',
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/pdf/unlock-pdf/service.ts b/src/pages/tools/pdf/unlock-pdf/service.ts
new file mode 100644
index 0000000..3068cee
--- /dev/null
+++ b/src/pages/tools/pdf/unlock-pdf/service.ts
@@ -0,0 +1,23 @@
+import { InitialValuesType } from './types';
+import { loadPDFData } from '../utils';
+import { unlockWithGhostScript } from '../../../../lib/ghostscript/worker-init';
+
+export async function unlockPdf(
+ pdfFile: File,
+ options: InitialValuesType
+): Promise {
+ // Check if file is a PDF
+ if (pdfFile.type !== 'application/pdf') {
+ throw new Error('The provided file is not a PDF');
+ }
+
+ const dataObject = {
+ psDataURL: URL.createObjectURL(pdfFile),
+ speed: options.speed
+ };
+ const protectedFileUrl: string = await unlockWithGhostScript(dataObject);
+ return await loadPDFData(
+ protectedFileUrl,
+ pdfFile.name.replace('.pdf', '-unlocked.pdf')
+ );
+}
diff --git a/src/pages/tools/pdf/unlock-pdf/types.ts b/src/pages/tools/pdf/unlock-pdf/types.ts
new file mode 100644
index 0000000..8e1ddc1
--- /dev/null
+++ b/src/pages/tools/pdf/unlock-pdf/types.ts
@@ -0,0 +1,5 @@
+export type Speed = 'fast' | 'normal' | 'slow';
+
+export type InitialValuesType = {
+ speed: Speed;
+};