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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Python CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.10, 3.11]
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 'permissions:|actions/checkout|persist-credentials' .github/workflows
repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
gh api "repos/${repo}/actions/permissions/workflow"

Repository: ToniBig/encryption-decryption-tool

Length of output: 736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow ---'
cat -n .github/workflows/ci.yaml

printf '%s\n' '--- workflow files and credential-related references ---'
git ls-files '.github/workflows'
rg -n -C 3 'GITHUB_TOKEN|github\.token|secrets\.|persist-credentials|git config|git remote|permissions:|checkout|pytest|unittest|python ' .github/workflows . 2>/dev/null | head -n 300

Repository: ToniBig/encryption-decryption-tool

Length of output: 4119


Disable persisted checkout credentials and restrict token permissions.

actions/checkout@v4 persists the token in local Git configuration by default. The subsequent dependency installation and pytest execution can access it. Set persist-credentials: false and permissions: contents: read.

🧰 Tools
🪛 zizmor (1.29.0)

[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yaml at line 16, Update the CI workflow’s
actions/checkout@v4 step to set persist-credentials to false, and configure the
job or workflow permissions to grant only contents: read.

Source: Linters/SAST tools

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
Comment on lines +18 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '14,21p' .github/workflows/ci.yaml
gh api repos/actions/setup-python/releases/latest --jq '.tag_name'

Repository: ToniBig/encryption-decryption-tool

Length of output: 460


🌐 Web query:

actions/setup-python v4 v6 v7 GitHub Actions compatibility requirements

💡 Result:

The actions/setup-python action versions v4, v6, and v7 all function to configure Python environments in GitHub Actions workflows, but they differ in their internal dependencies and runner requirements. Compatibility Requirements: - Runner Version: Starting with v6.0.0 and continuing into v7.x, the action requires the GitHub Actions runner to be on version v2.327.1 or later [1][2][3]. This is due to the upgrade from Node 20 to Node 24 [1][2][3]. - Implementation: Both v6 and v7 migrated action internals to ECMAScript Modules (ESM) to maintain compatibility with updated @actions/* packages [4][2][3]. Version Differences: - v7: The most recent major version (released July 2026), featuring dependency upgrades (including @actions/cache 6.2.0) and the removal of the pip-install input [5]. - v6: Introduced the upgrade to Node 24, which mandated the runner version requirement [2][3]. - v4: An older major version that lacks the recent ESM migration and Node 24-based runner requirements [6]. For all versions, it is recommended to explicitly set the python-version or python-version-file input rather than relying on the default system path, as the default version provided on GitHub-hosted runners can change [4][3]. Additionally, users can use the check-latest flag if they want to ensure they are using the latest available patch version of a specified Python version [7].

Citations:


🏁 Script executed:

#!/bin/bash
# Check the releases of setup-python to understand version history and recommendations
gh api repos/actions/setup-python/releases --jq '[.[] | {tag_name, published_at, prerelease}] | .[0:5]'

Repository: ToniBig/encryption-decryption-tool

Length of output: 569


Update actions/setup-python to a compatible version before merge.

actions/setup-python@v4 is incompatible with current GitHub Actions runners. Starting with v6.0.0, this action requires GitHub Actions runner version 2.327.1 or later due to the Node 24 upgrade. Update to actions/setup-python@v7, the current stable release.

Proposed fix
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v7
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
🧰 Tools
🪛 actionlint (1.7.12)

[error] 18-18: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yaml around lines 18 - 20, Update the setup-python
action reference in the CI workflow from actions/setup-python@v4 to the current
stable actions/setup-python@v7, preserving the existing matrix.python-version
configuration.

Source: Linters/SAST tools

- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install pytest
- name: Run tests
run: |
pytest -q
59 changes: 45 additions & 14 deletions crypto.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,69 @@
from cryptography.fernet import Fernet
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
import base64
import os

import binascii
from typing import Optional

def generate_key(password: str, salt: bytes) -> bytes:
"""Derive a Fernet key from password+salt using PBKDF2-HMAC-SHA256.

Raises ValueError for invalid password input.
"""
if not isinstance(password, str) or password == "":
raise ValueError("Password must be a non-empty string")
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
iterations=100000, # preserve compatibility
backend=default_backend()
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
key = base64.urlsafe_b64encode(kdf.derive(password.encode('utf-8')))
return key

# Function to encrypt a message


def encrypt_message(message: str, password: str) -> str:
"""Encrypt a message using a password.

Output format: base64url(salt || fernet_token)
"""
if not isinstance(message, str):
raise ValueError("Message must be a string")
if not isinstance(password, str) or password == "":
raise ValueError("Password must be a non-empty string")
salt = os.urandom(16)
key = generate_key(password, salt)
f = Fernet(key)
encrypted_message = f.encrypt(message.encode())
return base64.urlsafe_b64encode(salt + encrypted_message).decode()
encrypted_message = f.encrypt(message.encode('utf-8'))
return base64.urlsafe_b64encode(salt + encrypted_message).decode('utf-8')

# Function to decrypt a message
def decrypt_message(encrypted_message: str, password: str) -> str:
"""Decrypt a message produced by encrypt_message.

Raises ValueError for invalid inputs or when decryption fails.
"""
if not isinstance(encrypted_message, str) or encrypted_message.strip() == "":
raise ValueError("Encrypted message must be a non-empty string")
if not isinstance(password, str) or password == "":
raise ValueError("Password must be a non-empty string")

def decrypt_message(encrypted_message: str, password: str) -> str:
decoded_message = base64.urlsafe_b64decode(encrypted_message)
salt, encrypted_message = decoded_message[:16], decoded_message[16:]
data = encrypted_message.strip()
try:
decoded = base64.urlsafe_b64decode(data)
except (binascii.Error, ValueError) as exc:
raise ValueError("Invalid encrypted data: not valid base64") from exc
Comment on lines +52 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import base64
import binascii

payload = b"salt-and-token-bytes"
encoded = base64.urlsafe_b64encode(payload).decode()
tampered = encoded[:4] + "!" + encoded[4:]

assert base64.urlsafe_b64decode(tampered) == payload

try:
    base64.b64decode(tampered, altchars=b"-_", validate=True)
except binascii.Error:
    print("Strict decoding rejects the injected character.")
else:
    raise SystemExit("Strict decoding unexpectedly accepted malformed Base64.")
PY

Repository: ToniBig/encryption-decryption-tool

Length of output: 220


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
fd -i '^crypto\.py$|^test.*|.*test.*' -t f . | head -80

printf '%s\n' '--- crypto.py outline ---'
ast-grep outline crypto.py --view compact 2>/dev/null || true

printf '%s\n' '--- crypto.py relevant source ---'
cat -n crypto.py | sed -n '1,90p'

printf '%s\n' '--- decoder and decrypt usages ---'
rg -n -C 3 'urlsafe_b64decode|b64decode|decrypt_message|encrypt_message' . --glob '!node_modules' --glob '!dist' --glob '!build'

printf '%s\n' '--- standalone decoder behavior ---'
python3 - <<'PY'
import base64
import binascii

payload = b"salt-and-token-bytes"
encoded = base64.urlsafe_b64encode(payload).decode()
tampered = encoded[:4] + "!" + encoded[4:]

print("encoded:", encoded)
print("tampered:", tampered)
print("urlsafe result:", base64.urlsafe_b64decode(tampered))
try:
    base64.b64decode(tampered, altchars=b"-_", validate=True)
except binascii.Error as exc:
    print("strict result: rejected:", type(exc).__name__, str(exc))
else:
    raise SystemExit("strict result: accepted malformed Base64")
PY

Repository: ToniBig/encryption-decryption-tool

Length of output: 7823


Reject non-alphabet Base64 characters.

base64.urlsafe_b64decode discards an inserted !, so malformed ciphertext can decrypt successfully. Use base64.b64decode(data, altchars=b"-_", validate=True) and add a regression test for this case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crypto.py` around lines 52 - 56, Update the Base64 decoding in the
encrypted-message flow to use base64.b64decode with altchars=b"-_" and
validate=True, preserving the existing ValueError conversion for malformed
input. Add a regression test that inserts a non-alphabet character such as "!"
into otherwise valid ciphertext and verifies it is rejected.


if len(decoded) <= 16:
raise ValueError("Invalid encrypted data: too short")

salt, token = decoded[:16], decoded[16:]
key = generate_key(password, salt)
f = Fernet(key)
return f.decrypt(encrypted_message).decode()
try:
plain = f.decrypt(token)
except InvalidToken as exc:
# Wrong password or corrupted token
raise ValueError("Decryption failed: invalid password or corrupted data") from exc
return plain.decode('utf-8')
39 changes: 39 additions & 0 deletions tests/test_crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest
from crypto import encrypt_message, decrypt_message

def test_roundtrip():
msg = "Hello, world!\nThis is a test."
pwd = "S3cureP@ssw0rd"
enc = encrypt_message(msg, pwd)
assert isinstance(enc, str) and enc != msg
dec = decrypt_message(enc, pwd)
assert dec == msg

def test_wrong_password_raises():
msg = "Secret"
pwd = "correcthorsebatterystaple"
bad = "wrongpassword"
enc = encrypt_message(msg, pwd)
with pytest.raises(ValueError):
decrypt_message(enc, bad)

def test_invalid_base64_raises():
with pytest.raises(ValueError):
decrypt_message("not-base64!!!", "pwd")

def test_empty_password_encrypt_raises():
with pytest.raises(ValueError):
encrypt_message("text", "")

def test_empty_password_decrypt_raises():
with pytest.raises(ValueError):
decrypt_message(" ", "")

def test_corrupted_ciphertext_raises():
msg = "Data"
pwd = "password"
enc = encrypt_message(msg, pwd)
# truncate the ciphertext to simulate corruption
corrupted = enc[:-10]
with pytest.raises(ValueError):
decrypt_message(corrupted, pwd)
Loading