-
Notifications
You must be signed in to change notification settings - Fork 0
UI polish and hardening #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
af362c1
8ca785f
40609d1
f5c0154
aa843d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||
| - name: Set up Python ${{ matrix.python-version }} | ||||||||||||||
| uses: actions/setup-python@v4 | ||||||||||||||
| with: | ||||||||||||||
| python-version: ${{ matrix.python-version }} | ||||||||||||||
|
Comment on lines
+18
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
💡 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 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
Proposed fix- uses: actions/setup-python@v4
+ uses: actions/setup-python@v7📝 Committable suggestion
Suggested change
🧰 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 AgentsSource: 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 | ||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.")
PYRepository: 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")
PYRepository: ToniBig/encryption-decryption-tool Length of output: 7823 Reject non-alphabet Base64 characters.
🤖 Prompt for AI Agents |
||
|
|
||
| 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') | ||
| 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) |
There was a problem hiding this comment.
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:
Repository: ToniBig/encryption-decryption-tool
Length of output: 736
🏁 Script executed:
Repository: ToniBig/encryption-decryption-tool
Length of output: 4119
Disable persisted checkout credentials and restrict token permissions.
actions/checkout@v4persists the token in local Git configuration by default. The subsequent dependency installation andpytestexecution can access it. Setpersist-credentials: falseandpermissions: 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
Source: Linters/SAST tools