Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ passifypdf --help
```


Sample Run:
Sample Run (Encrypt):
```bash
passifypdf -i input.pdf -o protected.pdf -p mySecretPassword

Expand All @@ -44,6 +44,15 @@ passifypdf -i input.pdf -o protected.pdf -p mySecretPassword
# PDF file encrypted successfully and saved as 'protected.pdf'
```

Sample Run (Decrypt):
```bash
passifypdf -i protected.pdf -o unlocked.pdf -p mySecretPassword --decrypt

# -------------------------Sample output----------------------
# Congratulations!
# PDF file decrypted successfully and saved as 'unlocked.pdf'
```

## Known Issues
If you have nay special chars(example: an emoji like Star 🌟) in the PDF file, it gives a minor complain during execution.
But it still does the job, so you can ingore that "char or object error" which you see in the output.
Expand All @@ -53,4 +62,4 @@ In general you can use passifypdf to protect your PDF files against chance attac
But you should not rely on this for mission-critical data or situation.

## Build & Run Locally
Visit [BUILD.md](BUILD.md)
Visit [BUILD.md](BUILD.md)
10 changes: 7 additions & 3 deletions docs/CLI_OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ The `passifypdf` tool accepts the following command-line arguments:
| Option | Long Option | Required | Description |
| :--- | :--- | :--- | :--- |
| `-i` | `--input` | Yes | Path to the input PDF file. |
| `-o` | `--output` | Yes | Path to the output (encrypted) PDF file. |
| `-p` | `--passwd` | Yes | Password to use for encryption. **Avoid passing sensitive passwords directly on the command line, as they may be exposed via process listings and shell history.** |
| `-o` | `--output` | Yes | Path to the output (encrypted/decrypted) PDF file. |
| `-p` | `--passwd` | Yes | Password to use for encryption/decryption. **Avoid passing sensitive passwords directly on the command line, as they may be exposed via process listings and shell history.** |
| | `--decrypt` | No | Decrypt an encrypted PDF instead of encrypting a plain PDF. |
| `-v` | `--version` | No | Show the program's version number and exit. |
| `-h` | `--help` | No | Show the help message and exit. |

## Example Usage

```bash
# Using the installed CLI command (after `poetry install`)
# Encrypt a PDF
passifypdf -i input.pdf -o protected.pdf -p mySecretPassword

# Decrypt a password-protected PDF
passifypdf -i protected.pdf -o unlocked.pdf -p mySecretPassword --decrypt
```
45 changes: 32 additions & 13 deletions passifypdf/cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import argparse


def get_arg_parser() -> argparse.ArgumentParser:
arg_parser = argparse.ArgumentParser(
description="Encrypt a PDF file with a password of your choice.",
epilog="For more information, visit: https://github.com/SUPAIDEAS/passifypdf"
)
arg_parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0")
arg_parser.add_argument("-i", "--input", required=True, help="Path to the input PDF file to be encrypted")
arg_parser.add_argument("-o", "--output", required=True, help="Path where the encrypted PDF file will be saved")
arg_parser.add_argument("-p", "--passwd", required=True, type=str, help="Password to encrypt the PDF file with")
return arg_parser
import argparse


def get_arg_parser() -> argparse.ArgumentParser:
arg_parser = argparse.ArgumentParser(
description=(
"Encrypt a PDF file with a password, or decrypt an encrypted PDF "
"using --decrypt."
),
epilog="For more information, visit: https://github.com/SUPAIDEAS/passifypdf",
)
arg_parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0")
arg_parser.add_argument("-i", "--input", required=True, help="Path to the input PDF file")
arg_parser.add_argument(
"-o",
"--output",
required=True,
help="Path where the processed PDF file (encrypted/decrypted) will be saved",
)
arg_parser.add_argument(
"-p",
"--passwd",
required=True,
type=str,
help="Password to encrypt/decrypt the PDF file",
)
arg_parser.add_argument(
"--decrypt",
action="store_true",
help="Decrypt an encrypted PDF instead of encrypting a plain PDF",
)
return arg_parser
91 changes: 67 additions & 24 deletions passifypdf/encryptpdf.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
"""Core PDF encryption module."""
"""Core PDF encryption and decryption module."""

import sys
from pathlib import Path
from typing import Union
from typing import Tuple, Union

from pypdf import PdfReader, PdfWriter
from pypdf.errors import PdfReadError

from .cli import get_arg_parser


def _load_pdf_reader(input_pdf: Union[str, Path]) -> Tuple[Path, PdfReader]:
"""Validate that input is an existing PDF file and return its reader."""
input_path = Path(input_pdf)
Comment on lines +13 to +15

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_pdf_reader is typed/structured to return (Path, PdfReader), but current callers use _, reader = ... and ignore the Path. Consider returning only the PdfReader (or using the path) to keep the helper’s contract minimal.

Copilot uses AI. Check for mistakes.
if not input_path.exists():
raise FileNotFoundError(f"Input file '{input_pdf}' not found.")

if not input_path.is_file():
raise IsADirectoryError(f"Input path '{input_pdf}' is not a file.")

invalid_pdf_error = ValueError(f"Input file '{input_pdf}' is not a valid PDF file.")
with input_path.open("rb") as source_file:
if source_file.read(5) != b"%PDF-":

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PDF “magic bytes” validation is too strict: checking only the first 5 bytes for "%PDF-" can reject otherwise valid PDFs where the header appears later in the first 1024 bytes (a case many PDF parsers tolerate). Consider relying on PdfReader(...) for validation, or scanning the first chunk (e.g., 1KB) for the header rather than requiring it at offset 0.

Suggested change
if source_file.read(5) != b"%PDF-":
header_chunk = source_file.read(1024)
if b"%PDF-" not in header_chunk:

Copilot uses AI. Check for mistakes.
raise invalid_pdf_error

try:
reader = PdfReader(input_path)
except PdfReadError as exc:
raise invalid_pdf_error from exc
Comment on lines +23 to +30

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_pdf_reader opens the input file to read the header, then PdfReader opens/parses it again. This adds redundant I/O for every call. If you keep the header check, consider doing it in a way that avoids double-opening (or just let PdfReader be the single source of truth for validity).

Copilot uses AI. Check for mistakes.

return input_path, reader


def encrypt_pdf(input_pdf: Union[str, Path], output_pdf: Union[str, Path], password: str) -> None:
"""
Encrypts a PDF file with a password.
Expand All @@ -21,32 +44,48 @@ def encrypt_pdf(input_pdf: Union[str, Path], output_pdf: Union[str, Path], passw
Raises:
FileNotFoundError: If the input PDF file does not exist.
IsADirectoryError: If the input path is a directory.
Exception: For other errors during processing.
ValueError: If the input file is not a valid PDF.
"""
input_path = Path(input_pdf)
if not input_path.exists():
raise FileNotFoundError(f"Input file '{input_pdf}' not found.")
_, reader = _load_pdf_reader(input_pdf)
writer = PdfWriter()

if not input_path.is_file():
raise IsADirectoryError(f"Input path '{input_pdf}' is not a file.")
for page in reader.pages:
writer.add_page(page)

try:
reader = PdfReader(input_path)
writer = PdfWriter()
writer.encrypt(password)

# Add all pages from the reader to the writer
for page in reader.pages:
writer.add_page(page)
with open(output_pdf, "wb") as f:
writer.write(f)

# Encrypt with a password
writer.encrypt(password)

# Write the encrypted PDF to a new PDF file passed as param
with open(output_pdf, "wb") as f:
writer.write(f)
def decrypt_pdf(input_pdf: Union[str, Path], output_pdf: Union[str, Path], password: str) -> None:
"""
Decrypts a password-protected PDF file.

except Exception as e:
raise Exception(f"Failed to encrypt PDF: {e}")
Args:
input_pdf (Union[str, Path]): Path to the encrypted input PDF file.
output_pdf (Union[str, Path]): Path to the decrypted output PDF file.
password (str): Password to decrypt the PDF with.

Raises:
FileNotFoundError: If the input PDF file does not exist.
IsADirectoryError: If the input path is a directory.
ValueError: If input is not a valid PDF, is not encrypted, or password is incorrect.
"""
_, reader = _load_pdf_reader(input_pdf)

if not reader.is_encrypted:
raise ValueError(f"Input file '{input_pdf}' is not encrypted.")

if reader.decrypt(password) == 0:
raise ValueError(f"Failed to decrypt '{input_pdf}'. Please provide the correct password.")

writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)

with open(output_pdf, "wb") as f:
writer.write(f)


def main() -> int:
Expand All @@ -60,13 +99,17 @@ def main() -> int:
args = arg_parser.parse_args()

try:
encrypt_pdf(args.input, args.output, args.passwd)
print(f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'")
if args.decrypt:
decrypt_pdf(args.input, args.output, args.passwd)
print(f"Congratulations!\nPDF file decrypted successfully and saved as '{args.output}'")
else:
encrypt_pdf(args.input, args.output, args.passwd)
print(f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'")
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
sys.exit(main())
Loading
Loading