Skip to content

Commit 4d507b3

Browse files
authored
Merge pull request #112 from 5haiqin/main-616
Added Encryption-Decryption Web-app : Secure File Encryption
2 parents 691c538 + 5ea6260 commit 4d507b3

14 files changed

+1204
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Blackbit Web Encryptor
2+
3+
A web-based file encryption and decryption system with a cyberpunk UI. Backed by Flask and Cryptography (Fernet), fully local and stateless — no database needed.
4+
5+
6+
## ✨ Features
7+
- Encrypt any uploaded file using Fernet (AES-128 in CBC mode with HMAC; URL-safe token).
8+
- Auto-generate a `.key` file for decryption later.
9+
- Decrypt by uploading the `.encrypted` file and its `.key`.
10+
- Temporary storage in `./temp/` with automatic cleanup.
11+
- Cyberpunk / hacker UI with:
12+
- Neon glowing theme
13+
- Matrix rain overlay
14+
- Three.js 3D background (neon grid + wireframe cube)
15+
- Typewriter status messages & live console logs
16+
- No external database, works entirely on your machine.
17+
18+
19+
## 🏗 Tech Stack
20+
- Backend: Python 3.10+, Flask, cryptography (Fernet), Flask-CORS, pathlib
21+
- Frontend: HTML, CSS, Vanilla JS, Three.js, Typed.js
22+
23+
24+
## 📦 Project Structure
25+
```
26+
.
27+
├─ app.py
28+
├─ requirements.txt
29+
├─ templates/
30+
│ └─ index.html
31+
└─ static/
32+
├─ css/
33+
│ └─ styles.css
34+
├─ js/
35+
│ └─ main.js
36+
└─ img/
37+
└─ favicon.svg
38+
```
39+
40+
A `temp/` folder is created automatically at runtime to store transient files.
41+
42+
43+
## 🚀 Getting Started
44+
45+
1) Install dependencies
46+
- It’s recommended to use a virtual environment.
47+
48+
Windows (PowerShell):
49+
```
50+
py -m venv .venv
51+
.\.venv\Scripts\Activate.ps1
52+
pip install -r requirements.txt
53+
```
54+
55+
macOS/Linux:
56+
```
57+
python3 -m venv .venv
58+
source .venv/bin/activate
59+
pip install -r requirements.txt
60+
```
61+
62+
2) Run the server
63+
```
64+
python app.py
65+
```
66+
67+
3) Open in browser
68+
```
69+
http://127.0.0.1:5000
70+
```
71+
72+
73+
## 🔐 How It Works
74+
- Encryption uses `cryptography.fernet.Fernet`. A new random key is generated per encryption request.
75+
- The uploaded file is encrypted in-memory, then saved as a temporary `.encrypted` file and a separate `.key` file.
76+
- Decryption takes the `.encrypted` file and `.key`, decrypts to the original contents, and returns the restored file.
77+
- Files in `./temp/` are named with a random ID and periodically cleaned (older than 15 minutes) on each operation.
78+
79+
80+
— ⚡ Blackbit v2.0
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
from __future__ import annotations
2+
3+
import io
4+
import os
5+
import re
6+
import time
7+
import uuid
8+
from datetime import datetime, timedelta
9+
from pathlib import Path
10+
from typing import Tuple
11+
12+
from cryptography.fernet import Fernet, InvalidToken
13+
from flask import (
14+
Flask,
15+
jsonify,
16+
render_template,
17+
request,
18+
send_from_directory,
19+
abort,
20+
)
21+
from flask_cors import CORS
22+
from werkzeug.utils import secure_filename
23+
24+
# -------------------------------------------------------------
25+
# Blackbit Web Encryptor - Flask Backend
26+
# -------------------------------------------------------------
27+
BASE_DIR = Path(__file__).resolve().parent
28+
TEMP_DIR = BASE_DIR / "temp"
29+
TEMP_DIR.mkdir(parents=True, exist_ok=True)
30+
31+
MAX_AGE_SECONDS = 15 * 60 # 15 minutes
32+
33+
app = Flask(__name__)
34+
CORS(app)
35+
36+
37+
# ------------------------ Utilities -------------------------
38+
39+
def _cleanup_temp(max_age_seconds: int = MAX_AGE_SECONDS) -> None:
40+
"""Remove files in temp older than max_age_seconds."""
41+
now = datetime.utcnow()
42+
for p in TEMP_DIR.glob("*"):
43+
try:
44+
if not p.is_file():
45+
continue
46+
mtime = datetime.utcfromtimestamp(p.stat().st_mtime)
47+
if now - mtime > timedelta(seconds=max_age_seconds):
48+
p.unlink(missing_ok=True)
49+
except Exception:
50+
# best-effort cleanup
51+
pass
52+
53+
54+
def _unique_name(suffix: str) -> str:
55+
return f"bb-{uuid.uuid4().hex}{suffix}"
56+
57+
58+
def _sanitize_download_name(name: str) -> str:
59+
# allow alphanum, space, dot, dash, underscore; strip others
60+
return re.sub(r"[^\w\-. ]+", "_", name)[:200] or "download.bin"
61+
62+
63+
# ------------------------ Routes ----------------------------
64+
65+
@app.route("/")
66+
def index():
67+
return render_template("index.html")
68+
69+
70+
@app.route("/encrypt", methods=["POST"])
71+
def encrypt_route():
72+
_cleanup_temp()
73+
74+
if "file" not in request.files:
75+
return jsonify({"ok": False, "error": "No file part in request"}), 400
76+
77+
up = request.files["file"]
78+
if up.filename == "":
79+
return jsonify({"ok": False, "error": "No file selected"}), 400
80+
81+
original_name = secure_filename(up.filename) or "file.bin"
82+
83+
try:
84+
t0 = time.perf_counter()
85+
raw = up.read()
86+
key = Fernet.generate_key()
87+
f = Fernet(key)
88+
enc = f.encrypt(raw)
89+
elapsed = time.perf_counter() - t0
90+
91+
# Store files with unique names
92+
enc_store = _unique_name(".encrypted")
93+
key_store = _unique_name(".key")
94+
95+
(TEMP_DIR / enc_store).write_bytes(enc)
96+
(TEMP_DIR / key_store).write_bytes(key)
97+
98+
# Friendly download names
99+
friendly_enc = f"{Path(original_name).name}.encrypted"
100+
friendly_key = f"blackbit_{Path(original_name).stem}.key"
101+
102+
return jsonify({
103+
"ok": True,
104+
"encrypted": {
105+
"store_name": enc_store,
106+
"url": f"/download/{enc_store}?name={_sanitize_download_name(friendly_enc)}",
107+
"display_name": friendly_enc,
108+
"size_bytes": len(enc),
109+
},
110+
"key": {
111+
"store_name": key_store,
112+
"url": f"/download/{key_store}?name={_sanitize_download_name(friendly_key)}",
113+
"display_name": friendly_key,
114+
"size_bytes": len(key),
115+
},
116+
"stats": {
117+
"original_name": original_name,
118+
"original_size": len(raw),
119+
"encrypted_size": len(enc),
120+
"elapsed_seconds": round(elapsed, 4),
121+
},
122+
"log": [
123+
"[OK] Key generated",
124+
"[OK] File encrypted successfully",
125+
],
126+
})
127+
except Exception as e:
128+
return jsonify({"ok": False, "error": str(e)}), 500
129+
130+
131+
@app.route("/decrypt", methods=["POST"])
132+
def decrypt_route():
133+
_cleanup_temp()
134+
135+
if "encrypted" not in request.files or "key" not in request.files:
136+
return jsonify({"ok": False, "error": "Missing file(s). Provide 'encrypted' and 'key'."}), 400
137+
138+
up_enc = request.files["encrypted"]
139+
up_key = request.files["key"]
140+
141+
if up_enc.filename == "":
142+
return jsonify({"ok": False, "error": "No encrypted file selected"}), 400
143+
if up_key.filename == "":
144+
return jsonify({"ok": False, "error": "No key file selected"}), 400
145+
146+
enc_name_in = secure_filename(up_enc.filename)
147+
try:
148+
key_bytes = up_key.read()
149+
f = Fernet(key_bytes)
150+
t0 = time.perf_counter()
151+
dec = f.decrypt(up_enc.read())
152+
elapsed = time.perf_counter() - t0
153+
154+
# Guess original friendly name
155+
base = Path(enc_name_in).name
156+
if base.lower().endswith(".encrypted"):
157+
friendly_dec = base[:-10] # remove ".encrypted"
158+
if not friendly_dec:
159+
friendly_dec = "restored.bin"
160+
else:
161+
friendly_dec = f"{Path(base).stem}.restored{Path(base).suffix}"
162+
163+
# Store with unique real name, but serve with friendly name
164+
ext = Path(friendly_dec).suffix
165+
store_name = _unique_name(ext if ext else ".bin")
166+
(TEMP_DIR / store_name).write_bytes(dec)
167+
168+
return jsonify({
169+
"ok": True,
170+
"decrypted": {
171+
"store_name": store_name,
172+
"url": f"/download/{store_name}?name={_sanitize_download_name(friendly_dec)}",
173+
"display_name": friendly_dec,
174+
"size_bytes": len(dec),
175+
},
176+
"stats": {
177+
"elapsed_seconds": round(elapsed, 4),
178+
},
179+
"log": [
180+
"[OK] Key accepted",
181+
"[OK] File decrypted successfully",
182+
],
183+
})
184+
185+
except InvalidToken:
186+
return jsonify({"ok": False, "error": "Invalid key or corrupted encrypted file."}), 400
187+
except Exception as e:
188+
return jsonify({"ok": False, "error": str(e)}), 500
189+
190+
191+
@app.route("/download/<path:filename>")
192+
def download(filename: str):
193+
# Enforce filename pattern: bb-<uuid>.something
194+
if not re.fullmatch(r"bb-[0-9a-f]{32}\.[\w\-.]+", filename or ""):
195+
abort(404)
196+
197+
fp = TEMP_DIR / filename
198+
if not fp.exists() or not fp.is_file():
199+
abort(404)
200+
201+
download_name = request.args.get("name") or filename
202+
download_name = _sanitize_download_name(download_name)
203+
204+
# Let Flask handle range requests and headers
205+
return send_from_directory(
206+
directory=str(TEMP_DIR),
207+
path=filename,
208+
as_attachment=True,
209+
download_name=download_name,
210+
mimetype="application/octet-stream",
211+
max_age=0,
212+
)
213+
214+
215+
# -------------------- Entrypoint -----------------------------
216+
217+
if __name__ == "__main__":
218+
# On Windows and most dev setups, binding to 127.0.0.1 is fine
219+
app.run(host="127.0.0.1", port=5000, debug=False)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Flask==3.0.3
2+
Werkzeug==3.0.3
3+
cryptography==43.0.3
4+
Flask-Cors==4.0.1

0 commit comments

Comments
 (0)