From dcabed5b7dd80475f4ea8d68dd15bb55ca295eb1 Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Mon, 27 Apr 2026 22:23:47 +0500 Subject: [PATCH] Add SFS. --- SFS/.gitignore | 18 ++ SFS/README.md | 73 ++++++++ SFS/backend/app/config.py | 11 ++ SFS/backend/app/init.py | 19 ++ SFS/backend/app/model.py | 5 + SFS/backend/app/routes.py | 46 +++++ SFS/backend/app/utils/auth.py | 57 ++++++ SFS/backend/app/utils/encrytiopn.py | 22 +++ SFS/backend/app/utils/storage.py | 16 ++ SFS/backend/backend/manage.py | 22 +++ .../backend/sfs_backend/api/__init__.py | 0 SFS/backend/backend/sfs_backend/api/admin.py | 18 ++ SFS/backend/backend/sfs_backend/api/apps.py | 5 + SFS/backend/backend/sfs_backend/api/cloud.py | 50 ++++++ .../backend/sfs_backend/api/encryption.py | 99 +++++++++++ .../api/migrations/0001_initial.py | 55 ++++++ .../sfs_backend/api/migrations/__init__.py | 0 SFS/backend/backend/sfs_backend/api/models.py | 65 +++++++ .../backend/sfs_backend/api/permissions.py | 10 ++ .../backend/sfs_backend/api/serializers.py | 44 +++++ SFS/backend/backend/sfs_backend/api/tests.py | 3 + .../backend/sfs_backend/api/tests_auth.py | 19 ++ .../sfs_backend/api/tests_encryption.py | 23 +++ .../backend/sfs_backend/api/urls/routes.py | 14 ++ .../sfs_backend/api/utils/encryption.py | 84 +++++++++ .../backend/sfs_backend/api/utils/jwt_auth.py | 9 + .../backend/sfs_backend/api/utils/rsa_keys.py | 27 +++ SFS/backend/backend/sfs_backend/api/views.py | 115 ++++++++++++ SFS/backend/backend/sfs_backend/manage.py | 22 +++ .../sfs_backend/sfs_backend/__init__.py | 0 .../backend/sfs_backend/sfs_backend/asgi.py | 16 ++ .../sfs_backend/sfs_backend/settings.py | 163 ++++++++++++++++++ .../backend/sfs_backend/sfs_backend/urls.py | 20 +++ .../backend/sfs_backend/sfs_backend/wsgi.py | 16 ++ SFS/backend/requirements.txt | 10 ++ SFS/backend/run.py | 6 + SFS/cloud/firebase_config.py | 16 ++ SFS/cloud/upload_download.py | 26 +++ SFS/doc/Methodology.md | 30 ++++ SFS/doc/README.md | 31 ++++ SFS/doc/System_Architecture.md | 34 ++++ SFS/doc/User_Manual.md | 28 +++ SFS/docs/API_Documentation.md | 15 ++ SFS/docs/Methodology.md | 13 ++ SFS/docs/System_Architecture.md | 24 +++ SFS/frontend/package.json | 12 ++ SFS/frontend/public/index.html | 11 ++ SFS/frontend/src/App.js | 22 +++ SFS/frontend/src/components/FileList.jsx | 33 ++++ SFS/frontend/src/components/FileUpload.jsx | 20 +++ SFS/frontend/src/components/Login.jsx | 39 +++++ SFS/frontend/src/components/Register.jsx | 32 ++++ SFS/frontend/src/components/SearchBar.js | 21 +++ SFS/frontend/src/index.js | 6 + SFS/frontend/src/pages/Dashboard.jsx | 24 +++ SFS/frontend/src/pages/Home.jsx | 11 ++ SFS/frontend/src/pages/Profile.js | 10 ++ SFS/frontend/src/services/api.js | 13 ++ SFS/frontend/src/services/authService.js | 11 ++ SFS/frontend/src/services/fileService.js | 21 +++ SFS/frontend/templates/base.html | 15 ++ SFS/frontend/templates/dashboard.html | 62 +++++++ SFS/frontend/templates/login.html | 30 ++++ SFS/requirements.txt | 10 ++ SFS/test/test_auth.py | 13 ++ SFS/test/test_encryption.py | 42 +++++ SFS/test/test_file_ops.py | 16 ++ SFS/tests/test_encryption.py | 21 +++ 68 files changed, 1894 insertions(+) create mode 100644 SFS/README.md create mode 100644 SFS/backend/backend/manage.py create mode 100644 SFS/backend/backend/sfs_backend/api/__init__.py create mode 100644 SFS/backend/backend/sfs_backend/api/admin.py create mode 100644 SFS/backend/backend/sfs_backend/api/apps.py create mode 100644 SFS/backend/backend/sfs_backend/api/cloud.py create mode 100644 SFS/backend/backend/sfs_backend/api/encryption.py create mode 100644 SFS/backend/backend/sfs_backend/api/migrations/0001_initial.py create mode 100644 SFS/backend/backend/sfs_backend/api/migrations/__init__.py create mode 100644 SFS/backend/backend/sfs_backend/api/models.py create mode 100644 SFS/backend/backend/sfs_backend/api/permissions.py create mode 100644 SFS/backend/backend/sfs_backend/api/serializers.py create mode 100644 SFS/backend/backend/sfs_backend/api/tests.py create mode 100644 SFS/backend/backend/sfs_backend/api/tests_auth.py create mode 100644 SFS/backend/backend/sfs_backend/api/tests_encryption.py create mode 100644 SFS/backend/backend/sfs_backend/api/urls/routes.py create mode 100644 SFS/backend/backend/sfs_backend/api/utils/encryption.py create mode 100644 SFS/backend/backend/sfs_backend/api/utils/jwt_auth.py create mode 100644 SFS/backend/backend/sfs_backend/api/utils/rsa_keys.py create mode 100644 SFS/backend/backend/sfs_backend/api/views.py create mode 100644 SFS/backend/backend/sfs_backend/manage.py create mode 100644 SFS/backend/backend/sfs_backend/sfs_backend/__init__.py create mode 100644 SFS/backend/backend/sfs_backend/sfs_backend/asgi.py create mode 100644 SFS/backend/backend/sfs_backend/sfs_backend/settings.py create mode 100644 SFS/backend/backend/sfs_backend/sfs_backend/urls.py create mode 100644 SFS/backend/backend/sfs_backend/sfs_backend/wsgi.py create mode 100644 SFS/backend/requirements.txt create mode 100644 SFS/cloud/firebase_config.py create mode 100644 SFS/cloud/upload_download.py create mode 100644 SFS/doc/Methodology.md create mode 100644 SFS/doc/README.md create mode 100644 SFS/doc/System_Architecture.md create mode 100644 SFS/doc/User_Manual.md create mode 100644 SFS/docs/API_Documentation.md create mode 100644 SFS/docs/Methodology.md create mode 100644 SFS/docs/System_Architecture.md create mode 100644 SFS/frontend/package.json create mode 100644 SFS/frontend/src/components/SearchBar.js create mode 100644 SFS/frontend/src/pages/Profile.js create mode 100644 SFS/frontend/src/services/api.js create mode 100644 SFS/frontend/templates/base.html create mode 100644 SFS/frontend/templates/dashboard.html create mode 100644 SFS/frontend/templates/login.html create mode 100644 SFS/test/test_auth.py create mode 100644 SFS/test/test_encryption.py create mode 100644 SFS/test/test_file_ops.py create mode 100644 SFS/tests/test_encryption.py diff --git a/SFS/.gitignore b/SFS/.gitignore index e69de29..b1064ef 100644 --- a/SFS/.gitignore +++ b/SFS/.gitignore @@ -0,0 +1,18 @@ +__pycache__/ +node_modules/ +.env +firebase_key.json +*.pyc +.DS_Store + +# Environment & local files +.env.local +/backend/sfs_backend/.env +/backups/ +*.sqlite3 +db.sqlite3 +staticfiles/ +venv/ +.venv/ +.vscode/ +*.log diff --git a/SFS/README.md b/SFS/README.md new file mode 100644 index 0000000..0b82f52 --- /dev/null +++ b/SFS/README.md @@ -0,0 +1,73 @@ +# Secure File System (SFS) + +This repository contains a Django + DRF backend for a Secure File System and a minimal frontend using Bootstrap and Axios. + +## Features + +- User registration + JWT authentication (djangorestframework-simplejwt) +- AES-256 encrypted files, RSA-2048 per-user key pairs, HMAC-SHA256 integrity +- Upload / download / delete files +- Local, Firebase and S3 storage options (local implemented) +- Activity logs + +## Quick setup (development) + +1. Create virtual environment and activate + +```powershell +python -m venv venv +.\venv\Scripts\Activate.ps1 +``` + +2. Install backend dependencies + +```powershell +pip install -r backend/requirements.txt +``` + +3. Setup env + +```powershell +copy backend/sfs_backend/.env.example backend/sfs_backend/.env +# edit .env if needed +``` + +4. Apply migrations and create superuser + +```powershell +cd backend/sfs_backend +python manage.py migrate +python manage.py createsuperuser +``` + +5. Run server + +```powershell +python manage.py runserver +``` + +Access: + +- API root: http://127.0.0.1:8000/api/ +- Frontend: http://127.0.0.1:8000/login.html +- Admin: http://127.0.0.1:8000/admin/ + +## Production notes + +- Set `DJANGO_DEBUG=False` and configure `POSTGRES_*` env vars for PostgreSQL +- Use a secure `DJANGO_SECRET_KEY` and handle FILE master key +- Configure proper CORS origins +- Serve static files with WhiteNoise or from a CDN + +## Project layout + +(see project description in your IDE) + +## Tests + +Run tests with + +```powershell +cd backend/sfs_backend +python manage.py test +``` diff --git a/SFS/backend/app/config.py b/SFS/backend/app/config.py index e69de29..4ed2d47 100644 --- a/SFS/backend/app/config.py +++ b/SFS/backend/app/config.py @@ -0,0 +1,11 @@ +import os + +class Config: + SECRET_KEY = "YOUR_SECRET_KEY" + JWT_SECRET = "JWT_SECRET_KEY" + + MONGO_URI = "mongodb://localhost:27017/" + DB_NAME = "SecureFileStore" + + # Firebase or AWS keys + CLOUD_BUCKET = "your-bucket" diff --git a/SFS/backend/app/init.py b/SFS/backend/app/init.py index e69de29..c7bbc73 100644 --- a/SFS/backend/app/init.py +++ b/SFS/backend/app/init.py @@ -0,0 +1,19 @@ +from flask import Flask +from flask_cors import CORS +from .config import Config +from .routes import routes_bp +from pymongo import MongoClient + +def create_app(): + app = Flask(__name__) + CORS(app) + + app.config.from_object(Config) + + # Connect MongoDB + app.db = MongoClient(Config.MONGO_URI)[Config.DB_NAME] + + # Register routes + app.register_blueprint(routes_bp) + + return app diff --git a/SFS/backend/app/model.py b/SFS/backend/app/model.py index e69de29..8b70596 100644 --- a/SFS/backend/app/model.py +++ b/SFS/backend/app/model.py @@ -0,0 +1,5 @@ +def get_user_collection(app): + return app.db["users"] + +def get_file_collection(app): + return app.db["files"] diff --git a/SFS/backend/app/routes.py b/SFS/backend/app/routes.py index e69de29..4b8cde4 100644 --- a/SFS/backend/app/routes.py +++ b/SFS/backend/app/routes.py @@ -0,0 +1,46 @@ +from flask import Blueprint, request, jsonify +from .models import get_user_collection, get_file_collection +from .utils.auth import register_user, login_user, token_required +from .utils.storage import upload_to_cloud, download_from_cloud +from .utils.encryption import encrypt_file, decrypt_file + +routes_bp = Blueprint("routes", __name__) + +@routes_bp.route("/register", methods=["POST"]) +def register(): + data = request.json + return register_user(data) + +@routes_bp.route("/login", methods=["POST"]) +def login(): + data = request.json + return login_user(data) + +@routes_bp.route("/upload", methods=["POST"]) +@token_required +def upload(user): + file = request.files["file"] + encrypted_file_path, encrypted_key = encrypt_file(file) + + cloud_url = upload_to_cloud(encrypted_file_path) + + files = get_file_collection(routes_bp.app) + files.insert_one({ + "owner": user["_id"], + "filename": file.filename, + "cloud_url": cloud_url, + "encrypted_key": encrypted_key + }) + + return jsonify({"msg": "File uploaded securely"}), 200 + +@routes_bp.route("/download/", methods=["GET"]) +@token_required +def download(user, file_id): + files = get_file_collection(routes_bp.app) + file_data = files.find_one({"_id": file_id}) + + cloud_file = download_from_cloud(file_data["cloud_url"]) + decrypted = decrypt_file(cloud_file, file_data["encrypted_key"]) + + return decrypted diff --git a/SFS/backend/app/utils/auth.py b/SFS/backend/app/utils/auth.py index e69de29..97cbe5b 100644 --- a/SFS/backend/app/utils/auth.py +++ b/SFS/backend/app/utils/auth.py @@ -0,0 +1,57 @@ +import jwt +import bcrypt +from flask import current_app, jsonify, request +from functools import wraps +from ..models import get_user_collection + +def register_user(data): + users = get_user_collection(current_app) + + hashed_pw = bcrypt.hashpw(data["password"].encode(), bcrypt.gensalt()) + + new_user = { + "username": data["username"], + "email": data["email"], + "password": hashed_pw, + } + + users.insert_one(new_user) + return jsonify({"msg": "User registered"}), 201 + + +def login_user(data): + users = get_user_collection(current_app) + user = users.find_one({"email": data["email"]}) + + if not user: + return jsonify({"error": "User not found"}), 404 + + if not bcrypt.checkpw(data["password"].encode(), user["password"]): + return jsonify({"error": "Wrong password"}), 401 + + token = jwt.encode( + {"email": user["email"]}, + current_app.config["JWT_SECRET"], + algorithm="HS256" + ) + return jsonify({"token": token}) + + +def token_required(f): + @wraps(f) + def wrapper(*args, **kwargs): + token = request.headers.get("Authorization") + + if not token: + return jsonify({"error": "Token missing"}), 401 + + try: + data = jwt.decode(token, current_app.config["JWT_SECRET"], algorithms=["HS256"]) + users = get_user_collection(current_app) + user = users.find_one({"email": data["email"]}) + except: + return jsonify({"error": "Token invalid"}), 401 + + return f(user, *args, **kwargs) + + return wrapper diff --git a/SFS/backend/app/utils/encrytiopn.py b/SFS/backend/app/utils/encrytiopn.py index e69de29..3d48786 100644 --- a/SFS/backend/app/utils/encrytiopn.py +++ b/SFS/backend/app/utils/encrytiopn.py @@ -0,0 +1,22 @@ +from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.PublicKey import RSA +from Crypto.Random import get_random_bytes +import base64 +import os + +def encrypt_file(file): + aes_key = get_random_bytes(32) + + cipher_aes = AES.new(aes_key, AES.MODE_EAX) + ciphertext, tag = cipher_aes.encrypt_and_digest(file.read()) + + encrypted_file_path = "encrypted_" + file.filename + + with open(encrypted_file_path, "wb") as f: + f.write(cipher_aes.nonce + tag + ciphertext) + + rsa_key = RSA.generate(2048) + cipher_rsa = PKCS1_OAEP.new(rsa_key.publickey()) + encrypted_key = cipher_rsa.encrypt(aes_key) + + return encrypted_file_path, base64.b64encode(encrypted_key).decode() diff --git a/SFS/backend/app/utils/storage.py b/SFS/backend/app/utils/storage.py index e69de29..10335d5 100644 --- a/SFS/backend/app/utils/storage.py +++ b/SFS/backend/app/utils/storage.py @@ -0,0 +1,16 @@ +import firebase_admin +from firebase_admin import credentials, storage + +cred = credentials.Certificate("firebase-service.json") +firebase_admin.initialize_app(cred, {"storageBucket": "your-bucket"}) + +def upload_to_cloud(filepath): + bucket = storage.bucket() + blob = bucket.blob(filepath) + blob.upload_from_filename(filepath) + return blob.public_url + +def download_from_cloud(url): + # simplified for demonstration + import requests + return requests.get(url).content diff --git a/SFS/backend/backend/manage.py b/SFS/backend/backend/manage.py new file mode 100644 index 0000000..eb6431e --- /dev/null +++ b/SFS/backend/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/SFS/backend/backend/sfs_backend/api/__init__.py b/SFS/backend/backend/sfs_backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SFS/backend/backend/sfs_backend/api/admin.py b/SFS/backend/backend/sfs_backend/api/admin.py new file mode 100644 index 0000000..e5eb3de --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from .models import FileMetadata, UserKeyPair, ActivityLog + +@admin.register(FileMetadata) +class FileMetadataAdmin(admin.ModelAdmin): + list_display = ('id', 'original_filename', 'owner', 'size', 'storage_backend', 'created_at') + readonly_fields = ('created_at', 'updated_at') + + +@admin.register(UserKeyPair) +class UserKeyPairAdmin(admin.ModelAdmin): + list_display = ('user',) + + +@admin.register(ActivityLog) +class ActivityLogAdmin(admin.ModelAdmin): + list_display = ('action', 'user', 'file', 'timestamp') + readonly_fields = ('timestamp',) diff --git a/SFS/backend/backend/sfs_backend/api/apps.py b/SFS/backend/backend/sfs_backend/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/SFS/backend/backend/sfs_backend/api/cloud.py b/SFS/backend/backend/sfs_backend/api/cloud.py new file mode 100644 index 0000000..87aaddb --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/cloud.py @@ -0,0 +1,50 @@ +import os +from django.conf import settings + +STORAGE_BASE = getattr(settings, 'STORAGE_ROOT', settings.BASE_DIR / 'storage') + + +def ensure_user_dir(user_id: int): + path = os.path.join(STORAGE_BASE, str(user_id)) + os.makedirs(path, exist_ok=True) + return path + + +def upload_to_local(owner_id: int, stored_filename: str, data: bytes) -> str: + user_dir = ensure_user_dir(owner_id) + path = os.path.join(user_dir, stored_filename) + with open(path, 'wb') as f: + f.write(data) + return path + + +def download_from_local(owner_id: int, stored_filename: str) -> bytes: + path = os.path.join(STORAGE_BASE, str(owner_id), stored_filename) + with open(path, 'rb') as f: + return f.read() + + +def delete_from_local(owner_id: int, stored_filename: str) -> None: + path = os.path.join(STORAGE_BASE, str(owner_id), stored_filename) + try: + os.remove(path) + except FileNotFoundError: + pass + + +# NOTE: firebase and s3 implementations are placeholders that show how to extend + +def upload_to_firebase(owner_id: int, stored_filename: str, data: bytes) -> str: + raise NotImplementedError('Firebase upload not implemented in this example') + + +def download_from_firebase(owner_id: int, stored_filename: str) -> bytes: + raise NotImplementedError('Firebase download not implemented in this example') + + +def upload_to_s3(owner_id: int, stored_filename: str, data: bytes) -> str: + raise NotImplementedError('S3 upload not implemented in this example') + + +def download_from_s3(owner_id: int, stored_filename: str) -> bytes: + raise NotImplementedError('S3 download not implemented in this example') diff --git a/SFS/backend/backend/sfs_backend/api/encryption.py b/SFS/backend/backend/sfs_backend/api/encryption.py new file mode 100644 index 0000000..a562bff --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/encryption.py @@ -0,0 +1,99 @@ +from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.PublicKey import RSA +from Crypto.Hash import HMAC, SHA256 +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad, unpad +import base64 + +# ---- Helpers ---- + +def _derive_key(key_material: bytes) -> bytes: + # Ensure 32 bytes AES key using SHA256 + h = SHA256.new() + h.update(key_material) + return h.digest() + + +# ---- RSA keypair ---- + +def generate_rsa_keypair(bits: int = 2048): + key = RSA.generate(bits) + private_pem = key.export_key(format='PEM') + public_pem = key.publickey().export_key(format='PEM') + return private_pem, public_pem + + +# ---- Private key protection (encrypted by server master key) ---- + +def encrypt_private_key(private_pem: bytes, master_key: bytes) -> bytes: + # Encrypt private_pem using AES-256-CBC and add HMAC-SHA256 for integrity. + key = _derive_key(master_key) + iv = get_random_bytes(16) + cipher = AES.new(key, AES.MODE_CBC, iv) + ciphertext = cipher.encrypt(pad(private_pem, AES.block_size)) + # HMAC over iv + ciphertext + h = HMAC.new(key, digestmod=SHA256) + h.update(iv + ciphertext) + tag = h.digest() + # store as: tag || iv || ciphertext (bytes) + return tag + iv + ciphertext + + +def decrypt_private_key(package: bytes, master_key: bytes) -> bytes: + key = _derive_key(master_key) + tag = package[:32] + iv = package[32:48] + ciphertext = package[48:] + h = HMAC.new(key, digestmod=SHA256) + h.update(iv + ciphertext) + h.verify(tag) + cipher = AES.new(key, AES.MODE_CBC, iv) + plain = unpad(cipher.decrypt(ciphertext), AES.block_size) + return plain + + +# ---- File encryption (AES-256-CBC + HMAC-SHA256) ---- + +def encrypt_file_bytes(plain: bytes) -> tuple[bytes, bytes, bytes, bytes]: + # returns (aes_key, iv, ciphertext, hmac_tag) + aes_key = get_random_bytes(32) # AES-256 + iv = get_random_bytes(16) + cipher = AES.new(aes_key, AES.MODE_CBC, iv) + ciphertext = cipher.encrypt(pad(plain, AES.block_size)) + h = HMAC.new(aes_key, digestmod=SHA256) + h.update(iv + ciphertext) + tag = h.digest() + return aes_key, iv, ciphertext, tag + + +def decrypt_file_bytes(aes_key: bytes, iv: bytes, ciphertext: bytes, tag: bytes) -> bytes: + h = HMAC.new(aes_key, digestmod=SHA256) + h.update(iv + ciphertext) + h.verify(tag) + cipher = AES.new(aes_key, AES.MODE_CBC, iv) + plain = unpad(cipher.decrypt(ciphertext), AES.block_size) + return plain + + +# ---- RSA wrap/unwrap for AES keys ---- + +def rsa_encrypt_key(aes_key: bytes, public_pem: bytes) -> bytes: + pub = RSA.import_key(public_pem) + cipher = PKCS1_OAEP.new(pub, hashAlgo=SHA256) + return cipher.encrypt(aes_key) + + +def rsa_decrypt_key(encrypted_key: bytes, private_pem: bytes) -> bytes: + priv = RSA.import_key(private_pem) + cipher = PKCS1_OAEP.new(priv, hashAlgo=SHA256) + return cipher.decrypt(encrypted_key) + + +# ---- Utilities ---- + +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + + +def b64decode(s: str) -> bytes: + return base64.b64decode(s.encode('utf-8')) diff --git a/SFS/backend/backend/sfs_backend/api/migrations/0001_initial.py b/SFS/backend/backend/sfs_backend/api/migrations/0001_initial.py new file mode 100644 index 0000000..a3a9f39 --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 6.0 on 2025-12-17 12:07 + +import api.models +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='FileMetadata', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('original_filename', models.CharField(max_length=1024)), + ('stored_filename', models.CharField(default=api.models.generate_stored_filename, max_length=64, unique=True)), + ('content_type', models.CharField(blank=True, max_length=255)), + ('size', models.BigIntegerField(default=0)), + ('encrypted_key', models.BinaryField()), + ('hmac', models.CharField(max_length=128)), + ('storage_backend', models.CharField(choices=[('local', 'Local'), ('firebase', 'Firebase'), ('s3', 'S3')], default='local', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted', models.BooleanField(default=False)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ActivityLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(choices=[('UPLOAD', 'Upload'), ('DOWNLOAD', 'Download'), ('DELETE', 'Delete'), ('LOGIN', 'Login'), ('LOGOUT', 'Logout')], max_length=16)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('ip_address', models.GenericIPAddressField(blank=True, null=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.filemetadata')), + ], + ), + migrations.CreateModel( + name='UserKeyPair', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('public_key', models.TextField()), + ('encrypted_private_key', models.BinaryField()), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='keypair', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/SFS/backend/backend/sfs_backend/api/migrations/__init__.py b/SFS/backend/backend/sfs_backend/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SFS/backend/backend/sfs_backend/api/models.py b/SFS/backend/backend/sfs_backend/api/models.py new file mode 100644 index 0000000..eac70eb --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/models.py @@ -0,0 +1,65 @@ +import os +import uuid +from django.db import models +from django.contrib.auth import get_user_model + +User = get_user_model() + +STORAGE_CHOICES = (("local", "Local"), ("firebase", "Firebase"), ("s3", "S3")) + + +def user_storage_path(instance, filename): + # stored files will be placed under storage// + return os.path.join(str(instance.owner.id), instance.stored_filename) + + +def generate_stored_filename(): + return uuid.uuid4().hex + + +class UserKeyPair(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='keypair') + public_key = models.TextField() + encrypted_private_key = models.BinaryField() + + def __str__(self): + return f"KeyPair({self.user.username})" + + +class FileMetadata(models.Model): + owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='files') + original_filename = models.CharField(max_length=1024) + stored_filename = models.CharField(max_length=64, unique=True, default=generate_stored_filename) + content_type = models.CharField(max_length=255, blank=True) + size = models.BigIntegerField(default=0) + encrypted_key = models.BinaryField() # AES key encrypted with user's RSA public key + hmac = models.CharField(max_length=128) + storage_backend = models.CharField(max_length=20, choices=STORAGE_CHOICES, default='local') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + deleted = models.BooleanField(default=False) + + def get_local_path(self, base_dir): + return os.path.join(base_dir, str(self.owner.id), self.stored_filename) + + def __str__(self): + return f"File({self.original_filename}) by {self.owner.username}" + + +class ActivityLog(models.Model): + ACTION_CHOICES = ( + ('UPLOAD', 'Upload'), + ('DOWNLOAD', 'Download'), + ('DELETE', 'Delete'), + ('LOGIN', 'Login'), + ('LOGOUT', 'Logout'), + ) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + action = models.CharField(max_length=16, choices=ACTION_CHOICES) + file = models.ForeignKey(FileMetadata, on_delete=models.SET_NULL, null=True, blank=True) + timestamp = models.DateTimeField(auto_now_add=True) + ip_address = models.GenericIPAddressField(null=True, blank=True) + + def __str__(self): + who = self.user.username if self.user else 'system' + return f"{self.action} by {who} at {self.timestamp}" diff --git a/SFS/backend/backend/sfs_backend/api/permissions.py b/SFS/backend/backend/sfs_backend/api/permissions.py new file mode 100644 index 0000000..786fd83 --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/permissions.py @@ -0,0 +1,10 @@ +from rest_framework import permissions + + +class IsOwnerOrReadOnly(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + # Read: allow if authenticated + if request.method in permissions.SAFE_METHODS: + return obj.owner == request.user + # Write: only owner + return obj.owner == request.user diff --git a/SFS/backend/backend/sfs_backend/api/serializers.py b/SFS/backend/backend/sfs_backend/api/serializers.py new file mode 100644 index 0000000..2805b7e --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/serializers.py @@ -0,0 +1,44 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model +from .models import FileMetadata, ActivityLog, UserKeyPair +from . import encryption +from django.conf import settings + +User = get_user_model() + + +class RegisterSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ('username', 'email', 'password') + + def create(self, validated_data): + username = validated_data['username'] + password = validated_data['password'] + email = validated_data.get('email', '') + user = User.objects.create_user(username=username, email=email, password=password) + # generate RSA keypair + priv, pub = encryption.generate_rsa_keypair() + master_key = settings.SECRET_KEY.encode('utf-8') + enc_priv = encryption.encrypt_private_key(priv, master_key) + UserKeyPair.objects.create(user=user, public_key=pub.decode('utf-8'), encrypted_private_key=enc_priv) + ActivityLog.objects.create(user=user, action='LOGIN') + return user + + +class FileMetadataSerializer(serializers.ModelSerializer): + class Meta: + model = FileMetadata + fields = ('id', 'original_filename', 'content_type', 'size', 'created_at', 'storage_backend') + + +class UploadSerializer(serializers.Serializer): + file = serializers.FileField() + storage_backend = serializers.ChoiceField(choices=[('local', 'Local'), ('firebase','Firebase'), ('s3','S3')], default='local') + + def validate_file(self, f): + if f.size <= 0: + raise serializers.ValidationError('Empty file') + return f diff --git a/SFS/backend/backend/sfs_backend/api/tests.py b/SFS/backend/backend/sfs_backend/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/SFS/backend/backend/sfs_backend/api/tests_auth.py b/SFS/backend/backend/sfs_backend/api/tests_auth.py new file mode 100644 index 0000000..0ff73f1 --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/tests_auth.py @@ -0,0 +1,19 @@ +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIClient +from django.contrib.auth import get_user_model + +User = get_user_model() + +class AuthTests(TestCase): + def setUp(self): + self.client = APIClient() + + def test_register_and_token(self): + url = reverse('register') + resp = self.client.post(url, {'username': 'tester', 'password': 'pass1234', 'email': 't@example.com'}) + self.assertEqual(resp.status_code, 201) + token_url = reverse('token_obtain_pair') + resp = self.client.post(token_url, {'username': 'tester', 'password': 'pass1234'}) + self.assertEqual(resp.status_code, 200) + self.assertIn('access', resp.data) diff --git a/SFS/backend/backend/sfs_backend/api/tests_encryption.py b/SFS/backend/backend/sfs_backend/api/tests_encryption.py new file mode 100644 index 0000000..268ea28 --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/tests_encryption.py @@ -0,0 +1,23 @@ +from django.test import TestCase +from . import encryption + +class EncryptionTests(TestCase): + def test_rsa_wrap_unwrap(self): + priv, pub = encryption.generate_rsa_keypair() + aes_key = b'secretkey1234567890123456abcd' # 32 bytes + enc = encryption.rsa_encrypt_key(aes_key, pub) + dec = encryption.rsa_decrypt_key(enc, priv) + self.assertEqual(dec, aes_key) + + def test_aes_encrypt_decrypt(self): + data = b'hello world' * 50 + aes_key, iv, ciphertext, tag = encryption.encrypt_file_bytes(data) + out = encryption.decrypt_file_bytes(aes_key, iv, ciphertext, tag) + self.assertEqual(out, data) + + def test_private_key_protection(self): + priv, pub = encryption.generate_rsa_keypair() + master = b'master-secret-key-should-be-32bytes' + package = encryption.encrypt_private_key(priv, master) + decrypted = encryption.decrypt_private_key(package, master) + self.assertEqual(decrypted, priv) diff --git a/SFS/backend/backend/sfs_backend/api/urls/routes.py b/SFS/backend/backend/sfs_backend/api/urls/routes.py new file mode 100644 index 0000000..27ae84f --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/urls/routes.py @@ -0,0 +1,14 @@ +from django.urls import path, include +from rest_framework import routers +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from api.views import FileViewSet, RegisterAPIView + +router = routers.DefaultRouter() +router.register(r'files', FileViewSet, basename='file') + +urlpatterns = [ + path('', include(router.urls)), + path('auth/register/', RegisterAPIView.as_view(), name='register'), + path('auth/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), +] diff --git a/SFS/backend/backend/sfs_backend/api/utils/encryption.py b/SFS/backend/backend/sfs_backend/api/utils/encryption.py new file mode 100644 index 0000000..da5cb9f --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/utils/encryption.py @@ -0,0 +1,84 @@ +from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.PublicKey import RSA +from Crypto.Hash import HMAC, SHA256 +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad, unpad +import base64 + +# Helpers +def _derive_key(key_material: bytes) -> bytes: + h = SHA256.new() + h.update(key_material) + return h.digest() + +# RSA keypair +def generate_rsa_keypair(bits: int = 2048): + key = RSA.generate(bits) + private_pem = key.export_key(format='PEM') + public_pem = key.publickey().export_key(format='PEM') + return private_pem, public_pem + +# Private key protection (encrypted by server master key) +def encrypt_private_key(private_pem: bytes, master_key: bytes) -> bytes: + key = _derive_key(master_key) + iv = get_random_bytes(16) + cipher = AES.new(key, AES.MODE_CBC, iv) + ciphertext = cipher.encrypt(pad(private_pem, AES.block_size)) + h = HMAC.new(key, digestmod=SHA256) + h.update(iv + ciphertext) + tag = h.digest() + return tag + iv + ciphertext + + +def decrypt_private_key(package: bytes, master_key: bytes) -> bytes: + key = _derive_key(master_key) + tag = package[:32] + iv = package[32:48] + ciphertext = package[48:] + h = HMAC.new(key, digestmod=SHA256) + h.update(iv + ciphertext) + h.verify(tag) + cipher = AES.new(key, AES.MODE_CBC, iv) + plain = unpad(cipher.decrypt(ciphertext), AES.block_size) + return plain + +# File encryption (AES-256-CBC + HMAC-SHA256) +def encrypt_file_bytes(plain: bytes): + aes_key = get_random_bytes(32) + iv = get_random_bytes(16) + cipher = AES.new(aes_key, AES.MODE_CBC, iv) + ciphertext = cipher.encrypt(pad(plain, AES.block_size)) + h = HMAC.new(aes_key, digestmod=SHA256) + h.update(iv + ciphertext) + tag = h.digest() + return aes_key, iv, ciphertext, tag + + +def decrypt_file_bytes(aes_key: bytes, iv: bytes, ciphertext: bytes, tag: bytes) -> bytes: + h = HMAC.new(aes_key, digestmod=SHA256) + h.update(iv + ciphertext) + h.verify(tag) + cipher = AES.new(aes_key, AES.MODE_CBC, iv) + plain = unpad(cipher.decrypt(ciphertext), AES.block_size) + return plain + +# RSA wrap/unwrap for AES keys +def rsa_encrypt_key(aes_key: bytes, public_pem: bytes) -> bytes: + pub = RSA.import_key(public_pem) + cipher = PKCS1_OAEP.new(pub, hashAlgo=SHA256) + return cipher.encrypt(aes_key) + + +def rsa_decrypt_key(encrypted_key: bytes, private_pem: bytes) -> bytes: + priv = RSA.import_key(private_pem) + cipher = PKCS1_OAEP.new(priv, hashAlgo=SHA256) + return cipher.decrypt(encrypted_key) + +# Utilities + +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + + +def b64decode(s: str) -> bytes: + return base64.b64decode(s.encode('utf-8')) diff --git a/SFS/backend/backend/sfs_backend/api/utils/jwt_auth.py b/SFS/backend/backend/sfs_backend/api/utils/jwt_auth.py new file mode 100644 index 0000000..3bc8d50 --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/utils/jwt_auth.py @@ -0,0 +1,9 @@ +from rest_framework_simplejwt.tokens import RefreshToken + + +def get_tokens_for_user(user): + refresh = RefreshToken.for_user(user) + return { + 'refresh': str(refresh), + 'access': str(refresh.access_token), + } diff --git a/SFS/backend/backend/sfs_backend/api/utils/rsa_keys.py b/SFS/backend/backend/sfs_backend/api/utils/rsa_keys.py new file mode 100644 index 0000000..9f7391b --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/utils/rsa_keys.py @@ -0,0 +1,27 @@ +from django.conf import settings +from ..models import UserKeyPair +from . import encryption + + +def create_and_store_user_keys(user): + priv, pub = encryption.generate_rsa_keypair() + master = settings.SECRET_KEY.encode('utf-8') + enc_priv = encryption.encrypt_private_key(priv, master) + UserKeyPair.objects.create(user=user, public_key=pub.decode('utf-8'), encrypted_private_key=enc_priv) + return pub.decode('utf-8') + + +def get_user_private_key(user): + try: + kp = user.keypair + except UserKeyPair.DoesNotExist: + return None + master = settings.SECRET_KEY.encode('utf-8') + return encryption.decrypt_private_key(kp.encrypted_private_key, master) + + +def get_user_public_key(user): + try: + return user.keypair.public_key.encode('utf-8') + except UserKeyPair.DoesNotExist: + return None diff --git a/SFS/backend/backend/sfs_backend/api/views.py b/SFS/backend/backend/sfs_backend/api/views.py new file mode 100644 index 0000000..b77094c --- /dev/null +++ b/SFS/backend/backend/sfs_backend/api/views.py @@ -0,0 +1,115 @@ +import os +from rest_framework import viewsets, status +from rest_framework.views import APIView +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework.decorators import action +from django.shortcuts import get_object_or_404 +from django.conf import settings +from django.db import models +from django.http import HttpResponse, FileResponse +from .models import FileMetadata, ActivityLog, UserKeyPair +from .serializers import RegisterSerializer, FileMetadataSerializer, UploadSerializer +from .utils import encryption as encryption_utils +from .utils import storage as storage_utils +from .utils.rsa_keys import get_user_private_key + + +class RegisterAPIView(APIView): + permission_classes = [AllowAny] + + def post(self, request): + serializer = RegisterSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.save() + return Response({'username': user.username}, status=status.HTTP_201_CREATED) + + +class FileViewSet(viewsets.ModelViewSet): + serializer_class = FileMetadataSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + qs = FileMetadata.objects.filter(owner=self.request.user, deleted=False) + q = self.request.query_params.get('q') + if q: + qs = qs.filter( + models.Q(original_filename__icontains=q) | + models.Q(content_type__icontains=q) + ) + return qs + + def create(self, request, *args, **kwargs): + upload_serializer = UploadSerializer(data=request.data) + upload_serializer.is_valid(raise_exception=True) + f = upload_serializer.validated_data['file'] + backend = upload_serializer.validated_data.get('storage_backend', 'local') + # Read file bytes + data = f.read() + # Encrypt file + aes_key, iv, ciphertext, tag = encryption_utils.encrypt_file_bytes(data) + # Wrap AES key with user's RSA public key + try: + keypair = request.user.keypair + except UserKeyPair.DoesNotExist: + return Response({'detail': 'User has no keypair'}, status=status.HTTP_400_BAD_REQUEST) + enc_aes_key = encryption_utils.rsa_encrypt_key(aes_key, keypair.public_key.encode('utf-8')) + # Store ciphertext as iv + ciphertext + stored_blob = iv + ciphertext + stored_filename = FileMetadata._meta.get_field('stored_filename').get_default() # uuid + # Save to storage + if backend == 'local': + storage_utils.upload_to_local(request.user.id, stored_filename, stored_blob) + else: + # placeholders: currently using local for simplicity + storage_utils.upload_to_local(request.user.id, stored_filename, stored_blob) + # Create metadata + meta = FileMetadata.objects.create( + owner=request.user, + original_filename=f.name, + stored_filename=stored_filename, + content_type=f.content_type if hasattr(f, 'content_type') else 'application/octet-stream', + size=len(data), + encrypted_key=enc_aes_key, + hmac=tag.hex(), + storage_backend=backend, + ) + ActivityLog.objects.create(user=request.user, action='UPLOAD', file=meta) + return Response(FileMetadataSerializer(meta).data, status=status.HTTP_201_CREATED) + + @action(detail=True, methods=['get']) + def download(self, request, pk=None): + meta = get_object_or_404(FileMetadata, pk=pk, deleted=False) + if meta.owner != request.user: + return Response({'detail': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + # Retrieve stored file + blob = storage_utils.download_from_local(request.user.id, meta.stored_filename) + iv = blob[:16] + ciphertext = blob[16:] + tag = bytes.fromhex(meta.hmac) + # Decrypt AES key using user's private key + try: + keypair = request.user.keypair + except UserKeyPair.DoesNotExist: + return Response({'detail': 'User has no keypair'}, status=status.HTTP_400_BAD_REQUEST) + priv_pem = get_user_private_key(request.user) + if priv_pem is None: + return Response({'detail': 'User private key not found'}, status=status.HTTP_400_BAD_REQUEST) + aes_key = encryption_utils.rsa_decrypt_key(meta.encrypted_key.tobytes(), priv_pem) + # Decrypt file bytes + plain = encryption_utils.decrypt_file_bytes(aes_key, iv, ciphertext, tag) + ActivityLog.objects.create(user=request.user, action='DOWNLOAD', file=meta) + response = HttpResponse(plain, content_type=meta.content_type) + response['Content-Disposition'] = f'attachment; filename="{meta.original_filename}"' + return response + + def destroy(self, request, *args, **kwargs): + meta = self.get_object() + if meta.owner != request.user: + return Response({'detail': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN) + meta.deleted = True + meta.save() + # delete file from storage + cloud.delete_from_local(request.user.id, meta.stored_filename) + ActivityLog.objects.create(user=request.user, action='DELETE', file=meta) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/SFS/backend/backend/sfs_backend/manage.py b/SFS/backend/backend/sfs_backend/manage.py new file mode 100644 index 0000000..0d31957 --- /dev/null +++ b/SFS/backend/backend/sfs_backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sfs_backend.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/SFS/backend/backend/sfs_backend/sfs_backend/__init__.py b/SFS/backend/backend/sfs_backend/sfs_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SFS/backend/backend/sfs_backend/sfs_backend/asgi.py b/SFS/backend/backend/sfs_backend/sfs_backend/asgi.py new file mode 100644 index 0000000..547b44c --- /dev/null +++ b/SFS/backend/backend/sfs_backend/sfs_backend/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for sfs_backend project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sfs_backend.settings') + +application = get_asgi_application() diff --git a/SFS/backend/backend/sfs_backend/sfs_backend/settings.py b/SFS/backend/backend/sfs_backend/sfs_backend/settings.py new file mode 100644 index 0000000..b97ae7a --- /dev/null +++ b/SFS/backend/backend/sfs_backend/sfs_backend/settings.py @@ -0,0 +1,163 @@ +""" +Django settings for sfs_backend project. + +This settings file is configured for development (sqlite) and includes +production-ready guidelines (PostgreSQL config via env vars). +""" + +from pathlib import Path +import os +from datetime import timedelta +from dotenv import load_dotenv + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Load environment variables from BASE_DIR/.env if present +load_dotenv(BASE_DIR / '.env') + +# SECURITY +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'change-me-locally') +DEBUG = os.getenv('DJANGO_DEBUG', 'True') == 'True' +ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '127.0.0.1,localhost').split(',') + +# Application definition +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # Third party + 'rest_framework', + 'rest_framework.authtoken', + 'rest_framework_simplejwt', + 'corsheaders', + + # Local apps + 'api', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'sfs_backend.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / '..' / 'frontend' / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'sfs_backend.wsgi.application' + +# Database: default sqlite, but switch to postgres if POSTGRES_* env vars are set +if os.getenv('POSTGRES_DB'): + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('POSTGRES_DB'), + 'USER': os.getenv('POSTGRES_USER'), + 'PASSWORD': os.getenv('POSTGRES_PASSWORD'), + 'HOST': os.getenv('POSTGRES_HOST', 'localhost'), + 'PORT': os.getenv('POSTGRES_PORT', '5432'), + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } + } + +# Password validation +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Static files +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] + +# Storage root for encrypted files +STORAGE_ROOT = BASE_DIR / 'storage' + +# Default primary key +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Django REST Framework + JWT configuration +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=int(os.getenv('JWT_ACCESS_MINUTES', '60'))), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=int(os.getenv('JWT_REFRESH_DAYS', '7'))), + 'AUTH_HEADER_TYPES': ('Bearer',), +} + +# CORS +if os.getenv('CORS_ALLOWED_ORIGINS'): + CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS').split(',') +else: + CORS_ALLOW_ALL_ORIGINS = os.getenv('CORS_ALLOW_ALL_ORIGINS', 'True') == 'True' + +# Logging: basic console logger +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} + diff --git a/SFS/backend/backend/sfs_backend/sfs_backend/urls.py b/SFS/backend/backend/sfs_backend/sfs_backend/urls.py new file mode 100644 index 0000000..998dc86 --- /dev/null +++ b/SFS/backend/backend/sfs_backend/sfs_backend/urls.py @@ -0,0 +1,20 @@ +from django.contrib import admin +from django.urls import path, include +from django.views.generic import TemplateView +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('api.urls')), + # Frontend pages (simple template-based dashboard) + path('', TemplateView.as_view(template_name='login.html')), + path('login.html', TemplateView.as_view(template_name='login.html')), + path('register.html', TemplateView.as_view(template_name='register.html')), + path('dashboard.html', TemplateView.as_view(template_name='dashboard.html')), +] + +# Serve storage & static in dev +if settings.DEBUG: + urlpatterns += static('/storage/', document_root=getattr(settings, 'STORAGE_ROOT', settings.BASE_DIR / 'storage')) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/SFS/backend/backend/sfs_backend/sfs_backend/wsgi.py b/SFS/backend/backend/sfs_backend/sfs_backend/wsgi.py new file mode 100644 index 0000000..cdf418b --- /dev/null +++ b/SFS/backend/backend/sfs_backend/sfs_backend/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for sfs_backend project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sfs_backend.settings') + +application = get_wsgi_application() diff --git a/SFS/backend/requirements.txt b/SFS/backend/requirements.txt new file mode 100644 index 0000000..21d8638 --- /dev/null +++ b/SFS/backend/requirements.txt @@ -0,0 +1,10 @@ +Django==6.0 +djangorestframework +djangorestframework-simplejwt +pycryptodome +django-cors-headers +python-dotenv +firebase-admin +boto3 +psycopg2-binary +gunicorn diff --git a/SFS/backend/run.py b/SFS/backend/run.py index e69de29..488dae9 100644 --- a/SFS/backend/run.py +++ b/SFS/backend/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) diff --git a/SFS/cloud/firebase_config.py b/SFS/cloud/firebase_config.py new file mode 100644 index 0000000..c4a2d4a --- /dev/null +++ b/SFS/cloud/firebase_config.py @@ -0,0 +1,16 @@ +import firebase_admin +from firebase_admin import credentials, storage + +def initialize_firebase(): + # Load your Firebase Admin SDK key + cred = credentials.Certificate("firebase_key.json") + firebase_admin.initialize_app(cred, { + "storageBucket": "your-bucket-name.appspot.com" + }) + +def get_bucket(): + if not firebase_admin._apps: + initialize_firebase() + + bucket = storage.bucket() + return bucket diff --git a/SFS/cloud/upload_download.py b/SFS/cloud/upload_download.py new file mode 100644 index 0000000..da7ab1a --- /dev/null +++ b/SFS/cloud/upload_download.py @@ -0,0 +1,26 @@ +import uuid +from firebase_config import get_bucket + +def upload_file_to_firebase(encrypted_file_path): + bucket = get_bucket() + + unique_filename = str(uuid.uuid4()) + + blob = bucket.blob(f"SecureFiles/{unique_filename}") + blob.upload_from_filename(encrypted_file_path) + + blob.make_public() # optional + + return blob.public_url # URL stored in MongoDB + + +def download_file_from_firebase(file_url, output_path): + bucket = get_bucket() + + # Extract file name from URL + file_name = file_url.split("/")[-1] + + blob = bucket.blob(f"SecureFiles/{file_name}") + blob.download_to_filename(output_path) + + return output_path diff --git a/SFS/doc/Methodology.md b/SFS/doc/Methodology.md new file mode 100644 index 0000000..2f4a042 --- /dev/null +++ b/SFS/doc/Methodology.md @@ -0,0 +1,30 @@ +# Methodology – SecureFileStore + +## Step 1: User Authentication +- JWT-based login. +- User receives token for session security. + +## Step 2: Key Generation +- AES key generated per file. +- RSA public/private key per user. + +## Step 3: File Encryption +- File encrypted using AES. +- AES key encrypted using RSA. + +## Step 4: Upload Process +- Encrypted file uploaded to cloud. +- Metadata saved to MongoDB. + +## Step 5: Download Process +- Fetch AES encrypted key + file. +- User decrypts AES key using private RSA key. +- File decrypted locally. + +## Step 6: Search System +- Query MongoDB based on filename or owner. + +## Step 7: Testing +- Encryption tests +- Authentication tests +- Cloud upload tests diff --git a/SFS/doc/README.md b/SFS/doc/README.md new file mode 100644 index 0000000..7fae974 --- /dev/null +++ b/SFS/doc/README.md @@ -0,0 +1,31 @@ +# SecureFileStore + +A secure cloud-based encrypted file storage system using: +- AES (file encryption) +- RSA (key encryption) +- MongoDB +- Firebase/AWS S3 +- JWT Authentication + +## Features +✔ Client-side encryption +✔ Secure key management +✔ File search +✔ Cloud-based encrypted storage +✔ User authentication + +## How to Run + +### Backend +```py +cd backend +pip install -r requirements.txt +python run.py +``` + +### Frontend +```py +cd frontend +npm install +npm start +``` diff --git a/SFS/doc/System_Architecture.md b/SFS/doc/System_Architecture.md new file mode 100644 index 0000000..b5cc255 --- /dev/null +++ b/SFS/doc/System_Architecture.md @@ -0,0 +1,34 @@ +# System Architecture – SecureFileStore + +## Overview +SecureFileStore is a cloud-based encrypted file storage system that ensures: +- Client-side encryption +- AES for fast file encryption +- RSA for secure key management +- MongoDB for metadata +- Firebase/AWS for file storage +- JWT for secure authentication + +## Components +1. **Frontend (React)** + Manages UI, login, upload, search, download. + +2. **Backend (Flask/Django)** + - Handles API requests + - Performs encryption/decryption + - Connects to MongoDB + - Uploads encrypted files to Firebase/AWS + +3. **Cloud Storage** + Stores encrypted binary files. + +4. **Database (MongoDB)** + Stores: + - File owner + - File name + - Encrypted AES key + - Cloud URL + - Timestamps + +## Data Flow +User → Encrypt (AES) → Encrypt AES Key (RSA) → Upload to Cloud → Metadata saved → Download → Decrypt AES Key → Decrypt File diff --git a/SFS/doc/User_Manual.md b/SFS/doc/User_Manual.md new file mode 100644 index 0000000..844cd3c --- /dev/null +++ b/SFS/doc/User_Manual.md @@ -0,0 +1,28 @@ +# User Manual – SecureFileStore + +## 1. Registration +Enter name, email, password → RSA keys auto-generated. + +## 2. Login +Enter login details → receives JWT token. + +## 3. Upload File +- Select any file +- AES encryption happens +- Encrypted AES key stored +- File uploaded to cloud + +## 4. Download File +- Click on file +- System downloads encrypted file +- AES key decrypted +- File restored locally + +## 5. Search Files +Search by: +- File name +- Upload date +- File type + +## 6. Logout +JWT is removed from local storage. diff --git a/SFS/docs/API_Documentation.md b/SFS/docs/API_Documentation.md new file mode 100644 index 0000000..d90aec8 --- /dev/null +++ b/SFS/docs/API_Documentation.md @@ -0,0 +1,15 @@ +# API Documentation + +Base path: `/api/` + +Endpoints: + +- `POST /api/auth/register/` — create user. Body: `{username, password, email}` +- `POST /api/auth/token/` — obtain JWT. Body: `{username, password}` +- `POST /api/auth/token/refresh/` — refresh token +- `GET /api/files/` — list user's files +- `POST /api/files/` — upload file (multipart form `file`) +- `GET /api/files//download/` — download file +- `DELETE /api/files//` — delete file (soft delete + remove storage object) + +Authentication: Bearer token in `Authorization` header (JWT from `/auth/token/`). diff --git a/SFS/docs/Methodology.md b/SFS/docs/Methodology.md new file mode 100644 index 0000000..5a7318c --- /dev/null +++ b/SFS/docs/Methodology.md @@ -0,0 +1,13 @@ +# Methodology + +This project follows an API-first approach: + +- Implement REST endpoints with DRF +- Provide a simple frontend that consumes the API via Axios +- Keep encryption logic in a single module to make testing and auditing easier + +Testing strategy: + +- Unit tests for cryptography logic +- API tests for auth and file flows +- Integration tests for upload/download diff --git a/SFS/docs/System_Architecture.md b/SFS/docs/System_Architecture.md new file mode 100644 index 0000000..07c810a --- /dev/null +++ b/SFS/docs/System_Architecture.md @@ -0,0 +1,24 @@ +# SFS System Architecture + +Overview: + +- Django REST Framework backend provides secure file storage and management. +- Encryption: + - AES-256 (CBC) for file contents + - RSA-2048 for per-user key pair used to wrap AES keys + - HMAC-SHA256 for integrity of encrypted blobs +- Storage options: + - Local filesystem under `backend/storage/` (default) + - Firebase Storage (optional) + - AWS S3 (optional) + +Components: + +- `api` app: models for FileMetadata, UserKeyPair, ActivityLog and endpoints for auth and file operations +- `frontend` templates: simple Bootstrap UI using Axios +- `encryption.py`: cryptographic primitives and helpers + +Security considerations: + +- Private keys are encrypted at rest with a server master key (see `.env`) and never returned to the client +- All file operations require JWT authentication diff --git a/SFS/frontend/package.json b/SFS/frontend/package.json new file mode 100644 index 0000000..59c6b28 --- /dev/null +++ b/SFS/frontend/package.json @@ -0,0 +1,12 @@ +{ + "name": "securefilestore", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.4.0", + "axios": "^1.5.0", + "bootstrap": "^5.3.0" + } +} diff --git a/SFS/frontend/public/index.html b/SFS/frontend/public/index.html index e69de29..cdb90f2 100644 --- a/SFS/frontend/public/index.html +++ b/SFS/frontend/public/index.html @@ -0,0 +1,11 @@ + + + + + + SecureFileStore + + +
+ + diff --git a/SFS/frontend/src/App.js b/SFS/frontend/src/App.js index e69de29..a2fb8ec 100644 --- a/SFS/frontend/src/App.js +++ b/SFS/frontend/src/App.js @@ -0,0 +1,22 @@ +import React from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; + +import Login from "./components/Login"; +import Register from "./components/Register"; +import Dashboard from "./pages/Dashboard"; +import Home from "./pages/Home"; + +function App() { + return ( + + + } /> + } /> + } /> + } /> + + + ); +} + +export default App; diff --git a/SFS/frontend/src/components/FileList.jsx b/SFS/frontend/src/components/FileList.jsx index e69de29..07626b8 100644 --- a/SFS/frontend/src/components/FileList.jsx +++ b/SFS/frontend/src/components/FileList.jsx @@ -0,0 +1,33 @@ +import React, { useEffect, useState } from "react"; +import { getFiles } from "../services/fileService"; + +export default function FileList({ files: initialFiles }) { + const [files, setFiles] = useState(initialFiles || []); + + useEffect(() => { + if (!initialFiles) { + async function load() { + const f = await getFiles(); + setFiles(f); + } + load(); + } else { + setFiles(initialFiles); + } + }, [initialFiles]); + + return ( +
+

Your Files

+ {files && files.map((f) => ( +
+
{f.original_filename}
+
{f.size} bytes
+
+ Download +
+
+ ))} +
+ ); +} diff --git a/SFS/frontend/src/components/FileUpload.jsx b/SFS/frontend/src/components/FileUpload.jsx index e69de29..9ac1328 100644 --- a/SFS/frontend/src/components/FileUpload.jsx +++ b/SFS/frontend/src/components/FileUpload.jsx @@ -0,0 +1,20 @@ +import React, { useState } from "react"; +import { uploadFile } from "../services/fileService"; + +export default function FileUpload() { + const [file, setFile] = useState(null); + + async function handleUpload() { + if (!file) return alert("Select a file first"); + await uploadFile(file); + alert("File uploaded securely!"); + } + + return ( +
+

Upload File

+ setFile(e.target.files[0])} /> + +
+ ); +} diff --git a/SFS/frontend/src/components/Login.jsx b/SFS/frontend/src/components/Login.jsx index e69de29..5bc0700 100644 --- a/SFS/frontend/src/components/Login.jsx +++ b/SFS/frontend/src/components/Login.jsx @@ -0,0 +1,39 @@ +import React, { useState } from "react"; +import { login } from "../services/authService"; +import API from "../services/api"; + +export default function Login() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + async function handleLogin() { + const result = await login(username, password); + if (result.access) { + localStorage.setItem("sfs_token", result.access); + API.setToken(result.access); + window.location.href = "/dashboard"; + } else { + alert("Login failed"); + } + } + + return ( +
+

Login

+ + setUsername(e.target.value)} + />
+ + setPassword(e.target.value)} + />
+ + +
+ ); +} diff --git a/SFS/frontend/src/components/Register.jsx b/SFS/frontend/src/components/Register.jsx index e69de29..efbeee0 100644 --- a/SFS/frontend/src/components/Register.jsx +++ b/SFS/frontend/src/components/Register.jsx @@ -0,0 +1,32 @@ +import React, { useState } from "react"; +import { registerUser } from "../services/authService"; + +export default function Register() { + const [form, setForm] = useState({ + username: "", + email: "", + password: "" + }); + + function updateForm(e) { + setForm({ ...form, [e.target.name]: e.target.value }); + } + + async function submitRegister() { + await registerUser(form); + alert("Registered successfully!"); + window.location = '/login'; + } + + return ( +
+

Register

+ +
+
+
+ + +
+ ); +} diff --git a/SFS/frontend/src/components/SearchBar.js b/SFS/frontend/src/components/SearchBar.js new file mode 100644 index 0000000..25d6a07 --- /dev/null +++ b/SFS/frontend/src/components/SearchBar.js @@ -0,0 +1,21 @@ +import React, { useState } from "react"; + +export default function SearchBar({ onSearch }) { + const [q, setQ] = useState(""); + return ( +
+ setQ(e.target.value)} + /> + +
+ ); +} diff --git a/SFS/frontend/src/index.js b/SFS/frontend/src/index.js index e69de29..71a354b 100644 --- a/SFS/frontend/src/index.js +++ b/SFS/frontend/src/index.js @@ -0,0 +1,6 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render(); diff --git a/SFS/frontend/src/pages/Dashboard.jsx b/SFS/frontend/src/pages/Dashboard.jsx index e69de29..9d52247 100644 --- a/SFS/frontend/src/pages/Dashboard.jsx +++ b/SFS/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,24 @@ +import FileUpload from "../components/FileUpload"; +import FileList from "../components/FileList"; +import SearchBar from "../components/SearchBar"; +import { getFiles } from "../services/fileService"; +import React, { useState } from "react"; + +export default function Dashboard() { + const [files, setFiles] = useState([]); + + async function handleSearch(q){ + const resp = await getFiles(q) + setFiles(resp) + } + + return ( +
+

Dashboard

+ + +
+ +
+ ); +} diff --git a/SFS/frontend/src/pages/Home.jsx b/SFS/frontend/src/pages/Home.jsx index e69de29..9207299 100644 --- a/SFS/frontend/src/pages/Home.jsx +++ b/SFS/frontend/src/pages/Home.jsx @@ -0,0 +1,11 @@ +export default function Home() { + return ( +
+

SecureFileStore

+

A secure file storage and encryption system.

+ + Login
+ Register +
+ ); +} diff --git a/SFS/frontend/src/pages/Profile.js b/SFS/frontend/src/pages/Profile.js new file mode 100644 index 0000000..0677d7c --- /dev/null +++ b/SFS/frontend/src/pages/Profile.js @@ -0,0 +1,10 @@ +import React from "react"; + +export default function Profile() { + return ( +
+

Profile

+

Profile management (email, change password) will go here.

+
+ ); +} diff --git a/SFS/frontend/src/services/api.js b/SFS/frontend/src/services/api.js new file mode 100644 index 0000000..0038303 --- /dev/null +++ b/SFS/frontend/src/services/api.js @@ -0,0 +1,13 @@ +import axios from "axios"; + +const API = axios.create({ + baseURL: "/", + headers: { "Content-Type": "application/json" }, +}); + +API.setToken = (token) => { + if (token) API.defaults.headers.common["Authorization"] = `Bearer ${token}`; + else delete API.defaults.headers.common["Authorization"]; +}; + +export default API; diff --git a/SFS/frontend/src/services/authService.js b/SFS/frontend/src/services/authService.js index e69de29..8c1dd81 100644 --- a/SFS/frontend/src/services/authService.js +++ b/SFS/frontend/src/services/authService.js @@ -0,0 +1,11 @@ +import API from "./api"; + +export async function login(username, password) { + const resp = await API.post("/api/auth/token/", { username, password }); + return resp.data; +} + +export async function registerUser(data) { + const resp = await API.post("/api/auth/register/", data); + return resp.data; +} diff --git a/SFS/frontend/src/services/fileService.js b/SFS/frontend/src/services/fileService.js index e69de29..1ee8240 100644 --- a/SFS/frontend/src/services/fileService.js +++ b/SFS/frontend/src/services/fileService.js @@ -0,0 +1,21 @@ +import API from "./api"; + +export async function uploadFile(file) { + const fd = new FormData(); + fd.append("file", file); + const resp = await API.post("/api/files/", fd, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return resp.data; +} + +export async function getFiles(q) { + const options = q ? { params: { q } } : undefined; + const resp = await API.get("/api/files/", options); + return resp.data; +} + +export async function downloadFile(id) { + // direct link + return `/api/files/${id}/download/`; +} diff --git a/SFS/frontend/templates/base.html b/SFS/frontend/templates/base.html new file mode 100644 index 0000000..d171cda --- /dev/null +++ b/SFS/frontend/templates/base.html @@ -0,0 +1,15 @@ + + + + + + SFS + + + + +
+
{% block content %}{% endblock %}
+
+ + diff --git a/SFS/frontend/templates/dashboard.html b/SFS/frontend/templates/dashboard.html new file mode 100644 index 0000000..1b1cfe9 --- /dev/null +++ b/SFS/frontend/templates/dashboard.html @@ -0,0 +1,62 @@ +{% extends 'base.html' %} +{% block content %} +
+

Dashboard

+
+ +
+
+
+
+
Upload
+
+
+ +
+
+
+
+
+
Files
+
NameSizeActions
+
+
+ +{% endblock %} diff --git a/SFS/frontend/templates/login.html b/SFS/frontend/templates/login.html new file mode 100644 index 0000000..eb5e301 --- /dev/null +++ b/SFS/frontend/templates/login.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} +{% block content %} +
+
+

Login

+
+
+
+ +
+
+ Create account +
+
+ +{% endblock %} diff --git a/SFS/requirements.txt b/SFS/requirements.txt index e69de29..21d8638 100644 --- a/SFS/requirements.txt +++ b/SFS/requirements.txt @@ -0,0 +1,10 @@ +Django==6.0 +djangorestframework +djangorestframework-simplejwt +pycryptodome +django-cors-headers +python-dotenv +firebase-admin +boto3 +psycopg2-binary +gunicorn diff --git a/SFS/test/test_auth.py b/SFS/test/test_auth.py new file mode 100644 index 0000000..09f79b7 --- /dev/null +++ b/SFS/test/test_auth.py @@ -0,0 +1,13 @@ +import unittest +from app.utils.auth import create_jwt_token, verify_jwt_token + +class TestAuth(unittest.TestCase): + + def test_jwt_creation_and_verification(self): + token = create_jwt_token({"user_id": "123"}) + decoded = verify_jwt_token(token) + self.assertEqual(decoded["user_id"], "123") + + +if __name__ == "__main__": + unittest.main() diff --git a/SFS/test/test_encryption.py b/SFS/test/test_encryption.py new file mode 100644 index 0000000..7b04bd2 --- /dev/null +++ b/SFS/test/test_encryption.py @@ -0,0 +1,42 @@ +import unittest +from app.utils.encryption import ( + generate_aes_key, + encrypt_file_aes, + decrypt_file_aes, + rsa_encrypt_key, + rsa_decrypt_key +) + +class TestEncryption(unittest.TestCase): + + def test_aes_key_generation(self): + key1 = generate_aes_key() + key2 = generate_aes_key() + self.assertNotEqual(key1, key2) + + def test_aes_encryption_decryption(self): + key = generate_aes_key() + input_file = "sample.txt" + encrypted_file = "sample.enc" + decrypted_file = "sample_out.txt" + + with open(input_file, "w") as f: + f.write("Hello SecureFileStore") + + encrypt_file_aes(input_file, encrypted_file, key) + decrypt_file_aes(encrypted_file, decrypted_file, key) + + with open(decrypted_file, "r") as f: + result = f.read() + + self.assertEqual(result, "Hello SecureFileStore") + + def test_rsa_key_encryption_decryption(self): + aes_key = generate_aes_key() + encrypted_key = rsa_encrypt_key(aes_key) + decrypted_key = rsa_decrypt_key(encrypted_key) + self.assertEqual(aes_key, decrypted_key) + + +if __name__ == "__main__": + unittest.main() diff --git a/SFS/test/test_file_ops.py b/SFS/test/test_file_ops.py new file mode 100644 index 0000000..dbddcff --- /dev/null +++ b/SFS/test/test_file_ops.py @@ -0,0 +1,16 @@ +import unittest +from cloud.upload_download import upload_file_to_firebase + +class TestFileOperations(unittest.TestCase): + + def test_file_upload(self): + test_file = "test_upload.txt" + with open(test_file, "w") as f: + f.write("Test file") + + url = upload_file_to_firebase(test_file) + self.assertIn("https://", url) + + +if __name__ == "__main__": + unittest.main() diff --git a/SFS/tests/test_encryption.py b/SFS/tests/test_encryption.py new file mode 100644 index 0000000..c26bcd5 --- /dev/null +++ b/SFS/tests/test_encryption.py @@ -0,0 +1,21 @@ +import os, sys +import unittest +sys.path.insert(0, os.path.abspath('backend/backend/sfs_backend')) +from api.utils import encryption + +class TestEncryption(unittest.TestCase): + def test_aes_encrypt_decrypt(self): + data = b'Hello SFS' * 10 + aes_key, iv, ciphertext, tag = encryption.encrypt_file_bytes(data) + out = encryption.decrypt_file_bytes(aes_key, iv, ciphertext, tag) + self.assertEqual(out, data) + + def test_rsa_wrap_unwrap(self): + priv, pub = encryption.generate_rsa_keypair() + aes_key = b'0'*32 + enc = encryption.rsa_encrypt_key(aes_key, pub) + dec = encryption.rsa_decrypt_key(enc, priv) + self.assertEqual(dec, aes_key) + +if __name__ == '__main__': + unittest.main()