מדריך הגנה על פרויקט הגמר

פרויקט CloudStorage · אחסון ענן עם שיתוף קבצים מוצפן
חלופת מערכות הגנת סייבר · 5 יח"ל · "תכנון ותכנות מערכות"
מדריך לימוד והכנה לבחינת ההגנה לפי המחוון של משרד החינוך תשפ"ג

איך להשתמש במדריך הזה

המדריך בנוי בדיוק לפי הסדר של המחוון של משרד החינוך:

קופסה אדומה = משהו שחסר בפרויקט. יש תשובת גיבוי בעברית להגיד אם הבוחן שואל. קופסה ירוקה = מימוש חזק. קופסה כתומה = מימוש חלקי / מקום לשיפור.

סקירה כללית של הפרויקט

CloudStorage היא מערכת אחסון קבצים בענן עם שיתוף מוצפן בין משתמשים. למערכת שני לקוחות שונים:

  1. לקוח Web (HTML/CSS/JS) — בדפדפן. העלאה, הורדה, חיפוש, שיתוף, ניהול תיקיות, סל מיחזור, פאנל אדמין, פרופיל ועיצוב כהה/בהיר.
  2. לקוח Desktop (Python + Tkinter) — אפליקציה מותקנת. שומרת סוקט TCP פתוח לשרת ומקבלת התראות בזמן אמת ("פלוני שיתף איתך קובץ").

השרת מורכב משתי שכבות בתוך אותו תהליך:

אחסון: מטא-דאטה ב-Firestore (NoSQL של Google), קבצים ב-Google Cloud Storage, מוצפנים בהצפנת מעטפה (Envelope Encryption) של Fernet + RSA-2048.

ארכיטקטורה ברמה גבוהה (תיאור טקסטואלי)
Browser   ──HTTPS─►  Flask :5000  ──┐
                                    │  אותו תהליך, אותו זיכרון
Desktop   ──TCP─────►  NotificationHub :5001
Desktop   ◄──push──── (events: shared / file_changed)
Desktop   ──HTTPS───►  Flask :5000  (להורדת קבצים)

                       │
                       ▼
              Firestore (מטא-דאטה)
              Google Cloud Storage (בתים מוצפנים)

מה הקושי האמיתי של הפרויקט?

שתי תשתיות תקשורת באותו שרת: HTTP/REST שמשרת קבצים, וסוקט TCP גולמי שמשרת התראות. שני הלקוחות (Web ו-Desktop) מדברים עם אותם משאבים, אבל כל אחד דרך הערוץ המתאים לו. זו דוגמה מצוינת ל-"מימוש פרוטוקול העברת הודעות מצד לצד הגיוני ע"י התלמיד" — דרישה 2 במחוון.

1. תכנות מונחה עצמים — 4+ מחלקות שונות

הדרישה היא לפחות 4 מחלקות שהתלמיד יצר, עם שימוש נכון ב-OOP: חלוקה למחלקות, הפרדה לוגית, קשרים בין מחלקות, הכלה והורשה.

המחלקות בפרויקט (יותר מ-4!)

מחלקהקובץ:שורהתפקידקשר OOP
StorageItemserver/models/storage_item.py:20מחלקת-על מופשטת (Abstract Base Class) לכל פריט אחסוןAbstraction, Encapsulation
FileItemserver/models/file_item.py:17קובץ — יורש מ-StorageItemInheritance + Polymorphism
DirectoryItemserver/models/directory_item.py:24תיקייה — יורש מ-StorageItemInheritance + Polymorphism
Userserver/models/user.py:15משתמש רגיל — encapsulation לכל שדהEncapsulation
AdminUserserver/models/user.py:207מנהל מערכת — יורש מ-UserInheritance + Polymorphism
NotificationHubserver/socket_server.py:31מרכזת חיבורי סוקט וניהול threadsEncapsulation
NotificationClientdesktop/socket_client.py:19לקוח התראות בצד DesktopEncapsulation
ApiClientdesktop/api_client.py:19עטיפת REST לצד DesktopEncapsulation
DesktopAppdesktop/app.py:24ה-UI של TkinterEncapsulation, Composition (מכיל ApiClient + NotificationClient)

הפשטה (Abstraction) — מחלקה מופשטת

server/models/storage_item.py:11–76 · מחלקה אבסטרקטית עם @abstractmethod
from abc import ABC, abstractmethod

class StorageItem(ABC):
    """Common contract for File and Directory items."""

    def __init__(self, doc_id, owner, parent_directory_id=None, created_at=None):
        self._id = doc_id
        self._owner = owner
        self._parent_directory_id = parent_directory_id
        self._created_at = created_at or datetime.utcnow().isoformat()

    @abstractmethod
    def item_type(self):
        """Return 'file' or 'directory'."""

    @abstractmethod
    def display_name(self):
        """Human-readable name."""

    @abstractmethod
    def permanently_delete(self):
        """Hard-delete from GCS + Firestore."""

StorageItem היא מחלקה מופשטת — היא יורשת מ-ABC ומגדירה מתודות עם @abstractmethod. זה אומר שלא ניתן ליצור ממנה אובייקט ישירות; חייבים לרשת ממנה ולממש את כל המתודות המופשטות. זו הפשטה (Abstraction): היא חוזה — אומרת "כל פריט אחסון חייב לדעת להגיד את הסוג שלו, להציג שם, ולמחוק את עצמו". הקובץ (FileItem) והתיקייה (DirectoryItem) הם מימושים קונקרטיים של החוזה הזה.

הורשה (Inheritance) — FileItem ו-DirectoryItem יורשים מ-StorageItem

server/models/file_item.py:17–35 · FileItem(StorageItem)
class FileItem(StorageItem):
    """Represents an uploaded file."""

    def __init__(self, doc_id, owner, filename, stored_filename, gcs_path,
                 size, content_type, directory_id=None, uploaded_at=None,
                 wrapped_dek=None):
        # Inheritance — pass shared fields up to StorageItem.__init__
        super().__init__(doc_id, owner,
                         parent_directory_id=directory_id,
                         created_at=uploaded_at)
        self._filename = filename
        self._stored_filename = stored_filename
        self._gcs_path = gcs_path
        self._size = size
        self._content_type = content_type
        self._wrapped_dek = wrapped_dek

הקריאה super().__init__(...) מעבירה את השדות המשותפים למחלקת-העל. FileItem מקבל "בחינם" את _id, _owner, _parent_directory_id ו-_created_at + את המתודה check_ownership וה-soft_delete.

server/models/user.py:207–222 · AdminUser(User)
class AdminUser(User):
    """The single admin account."""

    def __init__(self, username, password_hash, first_name='', last_name='',
                 profile_picture=None, doc_id=None,
                 public_key=None, wrapped_private_key=None):
        super().__init__(username, password_hash, first_name, last_name,
                         profile_picture, doc_id, public_key, wrapped_private_key)

    # Polymorphism — override of User.is_admin()
    def is_admin(self):
        return True

AdminUser יורש מ-User. הוא דורס רק מתודה אחת: is_admin(). אצל User הרגיל המתודה הזו בודקת username == 'admin', אצל AdminUser היא תמיד מחזירה True. הקוד שמשתמש במשתמש (ה-decorator admin_required) קורא user.is_admin() בלי לדעת אם זה User או AdminUser — וזה בדיוק פולימורפיזם.

פולימורפיזם (Polymorphism) — אותה מתודה, התנהגות שונה לפי הסוג

server/models/storage_item.py:87–107 · soft_delete מוגדרת פעם אחת במחלקת-העל
def soft_delete(self, parent_directory_id_override=None, path=''):
    """Move this item into the recycle bin (30-day retention)."""
    expiration_date = datetime.utcnow() + timedelta(days=30)
    recycle_bin_data = {
        'item_id':   self._id,
        'item_type': self.item_type(),       # Polymorphism — subclass-specific
        'item_name': self.display_name(),    # Polymorphism — subclass-specific
        'original_data': self.to_dict(),     # Polymorphism — subclass-specific
        'deleted_by': self._owner,
        'deleted_at': datetime.utcnow().isoformat(),
        'expires_at': expiration_date.isoformat(),
        ...
    }
    ref = recycle_bin_collection.add(recycle_bin_data)
    self._collection_ref().document(self._id).delete()   # Polymorphism
    return ref[1].id

שימי לב: soft_delete נכתבת פעם אחת במחלקת-העל. היא קוראת ל-self.item_type(), self.display_name(), self._collection_ref(). בזמן ריצה Python בודק מהו האובייקט בפועל ומפעיל את המתודה הנכונה — אם זה FileItem היא מחזירה 'file' ומחקת מ-files; אם זה DirectoryItem היא מחזירה 'directory' ומחקת מ-directories. אותו קוד, התנהגות שונה. זה הלב של פולימורפיזם.

כימוס (Encapsulation) — שדות פרטיים והגישה דרך properties

server/models/user.py:22–87 · שדות פרטיים, properties וסיסמה
class User:
    def __init__(self, username, password_hash, ...):
        self._username = username
        self._password_hash = password_hash    # פרטי — קידומת underscore

    @property
    def username(self):
        return self._username                  # קריאה בלבד, אי-אפשר לדרוס מבחוץ

    def set_password(self, plain_password):
        # Encapsulation — הדרך היחידה להגדיר סיסמה היא דרך המתודה הזו,
        # שמבטיחה שהסיסמה תמיד נשמרת מוצפנת (hashed), אף פעם לא בטקסט גלוי.
        self._password_hash = generate_password_hash(plain_password, method='scrypt')

    def verify_password(self, plain_password):
        if not self._password_hash:
            return False
        return check_password_hash(self._password_hash, plain_password)

למה זה חשוב? כי אם הסיסמה הייתה public, מתכנת אחר בקוד היה יכול בטעות לכתוב user.password = "1234" ולשמור סיסמה בטקסט גלוי במסד הנתונים. הכימוס מאלץ אותו ללכת דרך set_password — שמכריחה hashing. הקופסה הסגורה הזו שומרת על אינווריאנט (חוק שאסור להפר): "סיסמאות תמיד מוצפנות".

הכלה (Composition) — אובייקט מכיל אובייקטים אחרים

desktop/app.py:24–37 · DesktopApp מכיל ApiClient ו-NotificationClient
class DesktopApp:
    def __init__(self, root):
        self.root = root
        self.api = None       # ייווצר ApiClient כשנתחבר
        self.nc = None        # ייווצר NotificationClient כשנתחבר
        self._shared_files = []
        ...

    def _connect_worker(self, server, username, password):
        api = ApiClient(server)        # אובייקט מוכל
        api.login(username, password)
        nc = NotificationClient()       # אובייקט מוכל
        nc.connect(host, SOCKET_PORT, api.token)

זו הכלה: DesktopApp לא יורש מ-ApiClient או מ-NotificationClient, אלא מחזיק בהם בתוכו. זה ההפך מהורשה — במקום "DesktopApp הוא ApiClient", זה "DesktopApp יש לו ApiClient". זה גם מאפשר לבדוק כל מחלקה בנפרד.

תשובות מוכנות לשאלות צפויות

2. תקשורת — סוקטים, רב-לקוחות, ופרוטוקול שכתבתי בעצמי

הדרישה היא שלושה דברים: (1) שרת ולקוח מבוססי סוקטים, (2) שרת מרובה לקוחות, (3) פרוטוקול הודעות הגיוני שאני פיתחתי.

הסוקט הגולמי (TCP) — לא HTTP

בנוסף ל-Flask REST API שעובד מעל HTTP, יש בפרויקט שרת סוקטים גולמי בפורט 5001. הוא לא משתמש ב-HTTP, ב-WebSocket, או בכל ספרייה חיצונית — רק socket מהספרייה התקנית של Python. הסיבה: HTTP הוא תקשורת בקשה-תשובה — הלקוח שואל, השרת עונה. אני רוצה שהשרת ייזום שליחה ללקוח כש"מישהו שיתף איתך קובץ" — וזה אפשרי רק על סוקט פתוח מתמשך.

server/socket_server.py:1–23 · תיעוד הפרוטוקול בראש הקובץ
"""
Raw TCP socket server for real-time push notifications.

This is intentionally separate from Flask: it listens on its own port (default 5001)
and speaks a tiny hand-rolled application protocol -- newline-delimited JSON, one
object per line -- over a plain TCP socket (Python's `socket` module).

Protocol (each message is one UTF-8 JSON object terminated by '\n'):

    Client -> Server : {"type": "auth", "token": ""}   (must be first message)
    Client -> Server : {"type": "ping"}
    Server -> Client : {"type": "auth_ok", "username": "bob"}
    Server -> Client : {"type": "auth_error", "error": "Invalid token"}  then close
    Server -> Client : {"type": "pong"}
    Server -> Client : {"type": "shared", ...}        (someone shared an item with you)
    Server -> Client : {"type": "file_changed", ...}  (one of your files changed)
"""

זה הפרוטוקול שלי. כל הודעה היא אובייקט JSON אחד, מסתיימת ב-\n. המסר הראשון מהלקוח חייב להיות אימות עם ה-JWT. אם האימות נכשל, השרת שולח auth_error וסוגר את הסוקט. בזמן ריצה אמצעי: ping/pong כדי שאדע אם הצד השני עוד שם.

הקמת השרת ו-bind/listen/accept

server/socket_server.py:82–105 · NotificationHub.start ו-accept loop
def start(self, host="127.0.0.1", port=5001):
    """Bind, listen, and spawn the accept loop on a daemon thread."""
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((host, port))
    srv.listen(16)
    self._server_socket = srv
    thread = threading.Thread(
        target=self._accept_loop, name="socket-accept-loop", daemon=True
    )
    thread.start()

def _accept_loop(self):
    while True:
        try:
            conn, addr = self._server_socket.accept()
        except OSError:
            break
        threading.Thread(
            target=self._handle_client,
            args=(conn, addr),
            name=f"socket-client-{addr}",
            daemon=True,
        ).start()

4 שלבים קלאסיים של שרת TCP:

  1. socket(AF_INET, SOCK_STREAM)AF_INET = IPv4, SOCK_STREAM = TCP (אם הייתי רוצה UDP זה היה SOCK_DGRAM).
  2. bind — קושר את הסוקט לכתובת IP ופורט.
  3. listen(16) — מתחיל להאזין, מכין תור של עד 16 חיבורים שמחכים.
  4. accept() — חוסם עד שלקוח מתחבר; מחזיר סוקט חדש לתקשורת עם אותו לקוח ספציפי.
SO_REUSEADDR מאפשר להפעיל את השרת מחדש מיד אחרי שעצרתי אותו, בלי לחכות שמערכת ההפעלה תשחרר את הפורט.

זו התשובה ל-"שרת מרובה לקוחות"

כל פעם ש-accept() מחזיר חיבור חדש — אני יוצרת thread חדש בדאמון (daemon=True) שמטפל באותו לקוח. בזמן שהthread הזה רץ, accept() חוזר לחכות ל-הלקוח הבא. כך מספר לקוחות יכולים להיות מחוברים בבו-זמנית.

הלקוח (Desktop)

desktop/socket_client.py:28–50 · NotificationClient.connect
def connect(self, host, port, token, timeout=10):
    """Open the socket, authenticate, and start the receive loop."""
    self._stop.clear()
    sock = socket.create_connection((host, port), timeout=timeout)
    sock.settimeout(None)
    self._sock = sock
    self._reader = _LineReader(sock)

    self._send({"type": "auth", "token": token})
    response = self._read_message()
    if not response or response.get("type") != "auth_ok":
        err = (response or {}).get("error", "authentication failed")
        self.close()
        raise ConnectionError(err)

    self._recv_thread = threading.Thread(target=self._recv_loop, daemon=True)
    self._recv_thread.start()
    self._ping_thread = threading.Thread(target=self._ping_loop, daemon=True)
    self._ping_thread.start()
    return response

הלקוח: יוצרת חיבור (socket.create_connection), שולחת מסר אימות עם ה-JWT, מחכה ל-auth_ok. אם הכל בסדר — מפעילה שני threads ברקע: אחד מקבל הודעות מהשרת, אחד שולח ping כל 20 שניות.

פריימינג של הודעות — איך יודעים איפה הודעה נגמרת?

server/socket_server.py:146–162 · _LineReader — פיצול לפי \n
class _LineReader:
    """Buffers bytes off a socket and yields one '\n'-terminated line at a time."""
    def __init__(self, conn):
        self._conn = conn
        self._buf = b""

    def readline(self):
        while b"\n" not in self._buf:
            chunk = self._conn.recv(4096)
            if not chunk:
                line, self._buf = self._buf, b""
                return line.decode("utf-8", "replace") if line else ""
            self._buf += chunk
        line, _, self._buf = self._buf.partition(b"\n")
        return line.decode("utf-8", "replace")

בעיה ב-TCP: TCP הוא זרם בייטים, לא הודעות. אם השרת שלח שתי הודעות, אני יכולה לקבל אותן ב-recv אחד; אם השרת שלח הודעה אחת ארוכה, אני יכולה לקבל אותה בשני recv-ים. אז אני צריכה שכבת פריימינג שתחלק את הזרם להודעות. הפתרון שלי: כל הודעה מסתיימת ב-\n, ויש לי באפר שאוסף בייטים ומחפש \n. כשיש — חותך את ההודעה הזו ושומר את השאר לבאפר לפעם הבאה. זו שכבת הפרוטוקול שאני יצרתי.

השרת דוחף הודעות (Server-Initiated Push)

server/socket_server.py:60–79 · push_to_user — הלב של "פוש"
def push_to_user(self, username, event):
    """Send one JSON event to every live socket for `username`."""
    line = (json.dumps(event) + "\n").encode("utf-8")
    with self._lock:
        conns = list(self._connections.get(username, ()))
    dead = []
    for conn in conns:
        try:
            conn.sendall(line)
        except OSError:
            dead.append(conn)
    for conn in dead:
        self.unregister(username, conn)
        try:
            conn.close()
        except OSError:
            pass

הפונקציה הזו נקראת מתוך נתיבי Flask — למשל ב-server/routes/sharing.py:130: notification_hub.push_to_user(share_with_username, {...}). כלומר: כשמשתמש עושה Share דרך ה-REST API, השרת מסתכל בטבלת החיבורים הפעילים, ואם המקבל מחובר ב-Desktop — שולח לו דחיפה דרך הסוקט הפתוח. זה לא היה אפשרי על HTTP טהור.

הצפנה בערוץ הסוקט — תשובה כנה

הסוקט בפורט 5001 הוא TCP גלוי — אין TLS עליו. ה-JWT עובר בהודעת ה-auth הראשונה בטקסט. איך אגיד את זה לבוחן?

"הסוקט עצמו הוא TCP גלוי כי הוא רץ על localhost בלבד — host='127.0.0.1' — וכל התקשורת בין שני התהליכים על אותו מחשב. שיפור עתידי שאני מתכננת זה לעטוף את הסוקט ב-ssl.SSLContext.wrap_socket בדומה לאיך שה-HTTP העתידי יעבור HTTPS. עדיין, ה-JWT עצמו חתום ב-HS256 ומכיל timestamps של תפוגה ופקיעה, כך שגם אם מישהו יחטוף אותו, יש לו תוקף מוגבל."

תשובות מוכנות לשאלות תקשורת

3. מערכת הפעלה — תהליכונים, קבצים, API

הדרישה: שימוש ב-Thread, וגישה למערכת קבצים או API או COM או רכיבים אחרים.

תהליכונים (Threads) — איפה משתמשים בהם בפרויקט?

איפהקובץ:שורהלמה
Accept loop של הסוקטsocket_server.py:89השרת לא יכול לחסום את התהליך הראשי. accept חוסם, אז הוא צריך thread משלו.
Handler לכל לקוחsocket_server.py:100כדי לתמוך במרובה לקוחות.
Receive loop של הלקוחdesktop/socket_client.py:46קולט הודעות מהשרת באופן רציף בלי לחסום את ה-UI.
Ping loop של הלקוחdesktop/socket_client.py:48שולח ping כל 20 שניות.
Connect worker ב-Tkinterdesktop/app.py:102פעולות רשת לא יכולות להתבצע ב-UI thread של Tkinter — הוא יקפא.
ThreadPoolExecutor — מחיקת תיקייהdirectory_item.py:78מחיקה מקבילה של עד 8 קבצים בו-זמנית מ-GCS.
ThreadPoolExecutor — Zip של תיקייהdirectory_item.py:280הורדה מקבילה של בתים מ-GCS לארגון לקובץ Zip.
ThreadPoolExecutor — העתקת תיקייהdirectory_item.py:213copy_blob מקבילי לכל קובץ.
ThreadPoolExecutor — מחיקת משתמשuser.py:302אדמין מוחק משתמש עם הרבה קבצים — לא רוצים שזה ייתקע.

דוגמת קוד — Thread פר לקוח

server/socket_server.py:94–105 · accept loop יוצר thread לכל לקוח
def _accept_loop(self):
    while True:
        try:
            conn, addr = self._server_socket.accept()
        except OSError:
            break  # listening socket closed -> shut down the loop
        threading.Thread(
            target=self._handle_client,
            args=(conn, addr),
            name=f"socket-client-{addr}",
            daemon=True,
        ).start()

daemon=True אומר שאם התהליך הראשי מסתיים, גם ה-threads האלה ייסגרו מיד. בלי daemon=True התהליך לא יוכל להיסגר עד שכל ה-threads ייסגרו לבד.

דוגמת קוד — ThreadPoolExecutor (Thread Pool)

server/models/directory_item.py:259–293 · הורדת קבצים מקבילית להכנת Zip
def fetch_blob(file_info):
    if not file_info['gcs_path']:
        return None
    blob = gcs_bucket.blob(file_info['gcs_path'])
    if not blob.exists():
        return None
    return (file_info['relative'],
            decrypt_stored_file(blob.download_as_bytes(), file_info.get('wrapped_dek')))

zip_buffer = io.BytesIO()
files_added = 0
total_bytes = 0
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
    with ThreadPoolExecutor(max_workers=8) as pool:
        futures = [pool.submit(fetch_blob, f) for f in all_files]
        for future in as_completed(futures):
            result = future.result()
            if result is None:
                continue
            relative, data = result
            total_bytes += len(data)
            if total_bytes > MAX_ZIP_BYTES:
                raise ZipLimitExceeded(...)
            zf.writestr(relative, data)
            files_added += 1

למה Thread Pool ולא threads רגילים? כי יצירת thread חדש לכל קובץ עולה לי הרבה במשאבים, וזה לא נכון להפעיל 200 threads בו-זמנית כש-GCS יכול לחנוק את ההורדות שלי. ThreadPoolExecutor(max_workers=8) מבטיח שמקסימום 8 הורדות יקרו במקביל, בעוד הקבצים האחרים מחכים בתור.
למה לא לעשות את הכתיבה ל-Zip במקביל? כי zipfile.ZipFile לא thread-safe. רק ההורדות (I/O ברשת) רצות במקביל; הכתיבה לזיפ נשארת ב-thread הראשי.

סנכרון — Lock

server/socket_server.py:40–58 · Lock על טבלת החיבורים
def __init__(self):
    self._connections = {}
    self._lock = threading.Lock()
    self._server_socket = None

def register(self, username, conn):
    with self._lock:
        self._connections.setdefault(username, set()).add(conn)

def unregister(self, username, conn):
    with self._lock:
        conns = self._connections.get(username)
        if conns:
            conns.discard(conn)
            if not conns:
                del self._connections[username]

טבלת ה-_connections משותפת בין כל ה-threads — accept loop רושם, handler מוסיף ומסיר, push_to_user קורא. אם שני threads ישנו אותה בו-זמנית — Race Condition. threading.Lock מבטיח שרק thread אחד יכול להיות בתוך with self._lock: בכל רגע נתון. זה סעיף קריטי (critical section).

גישה למערכת הקבצים

desktop/app.py:175–194 · בחירת קובץ ושמירה ל-disk עם filedialog
def _download_selected(self):
    sel = self.shared_list.curselection()
    if not sel:
        messagebox.showinfo("Download", "Select a file first.")
        return
    index = sel[0]
    f = self._shared_files[index]
    dest = filedialog.asksaveasfilename(initialfile=f.get("filename", "download"))
    if not dest:
        return
    try:
        self.api.download_shared_file(f["id"], dest)
    except ApiError as e:
        messagebox.showerror("Download failed", str(e))
        return
    self._log(f"Downloaded \"{f.get('filename', '')}\" to {dest}")

filedialog.asksaveasfilename פותח את החלון של מערכת ההפעלה לבחירת מיקום שמירה. אחר כך אני קוראת ל-self.api.download_shared_file שכותבת את הבתים ל-open(dest, "wb"). זו גישה ישירה למערכת הקבצים של המחשב — בדיוק מה שהמחוון דורש.

שימוש ב-API (חיצוני)

הפרויקט משתמש בשני APIs חיצוניים גדולים של Google: Firebase Admin SDK (לגישה ל-Firestore) ו-Google Cloud Storage SDK. שניהם מאומתים עם קובץ private key בפורמט JSON.

server/config.py:65–82 · אתחול ה-APIs
# Initialize Firebase Admin SDK
cred_path = os.path.join(os.path.dirname(__file__), 'cloudstorageproject-privatekey.json')
cred = credentials.Certificate(cred_path)
firebase_admin.initialize_app(cred)

# Get Firestore database
db = firestore.client()
users_collection = db.collection('users')
items_collection = db.collection('items')
directories_collection = db.collection('directories')
shares_collection = db.collection('shares')
favorites_collection = db.collection('favorites')
recycle_bin_collection = db.collection('recycle_bin')

# Initialize Google Cloud Storage
GCS_BUCKET_NAME = 'clientstorage-6978a.firebasestorage.app'
storage_client = storage.Client.from_service_account_json(cred_path)
gcs_bucket = storage_client.bucket(GCS_BUCKET_NAME)

זה שימוש ב-API: אני שולחת בקשות מאומתות לשירותים חיצוניים של Google ומקבלת תוצאות. בשונה ממסד נתונים מקומי שאני מנהלת בעצמי, ה-APIs האלה מטפלים בהפצה גלובלית, בעמידה בעומס, וב-replication.

⚠ חסר בפרויקט — תשובת גיבוי

אם הבוחן שואל ב-Windows API ספציפי (ctypes/win32com/COM/מיקרופון/מצלמה):

"המחוון מציע רשימת חלופות: 'גישה למערכת הקבצים ו/או שימוש ב-API ו/או רכיבי COM ו/או רכיבים אחרים'. אני מקיימת שתי חלופות: גישה למערכת הקבצים (ב-desktop/app.py דרך filedialog) ושימוש ב-APIs של Google. בנוסף אני משתמשת ב-Tkinter שהוא wrapper פייתוני ל-Tcl/Tk שמתממש דרך מנגנוני ה-Window Manager של מערכת ההפעלה (ב-Windows זה GDI/User32). הטרייד-אוף לבחירה הזו: API של Google נותן לי גלובליות ועמידה בעומס אמיתיים, מה שאני לא הייתי משיגה עם COM מקומי."

4. אבטחה — הצפנה וטיפול בפרצות

הדרישה: (1) הצפנה למידע רגיש העובר בתקשורת, (2) טיפול בפרצות אבטחה בפרויקט.

הצפנת מעטפה (Envelope Encryption) — מה זה ולמה

מודל ההגנה (Threat Model)

גוגל מצפינים את ה-GCS בעצמם, אבל המפתחות שלהם. אם תוקף יגנוב את ה-bucket (ע"י דליפה במפתחות שירות), הוא יוכל לקרוא את הקבצים. הפתרון שלי: שכבת הצפנה נוספת שאני שולטת בה. גם אם מישהו יגנוב את ה-bucket — הוא יראה Ciphertext בלי המפתחות שלי, ויהיה לו רק "בייטים אקראיים".

בפרויקט יש שלושה סוגי מפתחות:

  1. Master KEK (Key Encryption Key) — מפתח אחד סימטרי שהאפליקציה מחזיקה. הוא לא מצפין קבצים ישירות — הוא מצפין מפתחות אחרים.
  2. DEK פר-קובץ (Data Encryption Key) — מפתח סימטרי חדש שנוצר לכל קובץ שמועלה. הוא מצפין את בתי הקובץ עצמם. אז הוא נעטף ב-Master KEK ונשמר ב-Firestore.
  3. זוג RSA לכל משתמש — המפתח הציבורי משמש להצפנת DEK של קובץ שמשתפים איתי. המפתח הפרטי שלי (שמור עטוף ב-Master KEK) משחזר את ה-DEK כשאני מוריד.
server/utils/crypto.py:101–113 · הצפנה סימטרית עם DEK פר-קובץ
def generate_dek() -> bytes:
    """Generate a fresh per-file Data Encryption Key (a Fernet key)."""
    return Fernet.generate_key()

def encrypt_with_dek(plaintext: bytes, dek: bytes) -> bytes:
    """Encrypt file bytes under a per-file DEK."""
    return Fernet(dek).encrypt(plaintext)

def decrypt_with_dek(ciphertext: bytes, dek: bytes) -> bytes:
    """Decrypt file bytes that were encrypted under the given DEK."""
    return Fernet(dek).decrypt(ciphertext)

Fernet זה AES-128 ב-CBC mode + HMAC-SHA256 — הצפנה סימטרית מאומתת (Authenticated Encryption). זה אומר שלא רק הקובץ מוצפן, אלא יש חתימה שמוודאת שהוא לא שונה. אם מישהו מנסה לשנות בייט אחד ב-ciphertext, ה-HMAC ייכשל ו-decrypt יזרוק שגיאה.

server/utils/crypto.py:182–212 · RSA-OAEP לעטיפת DEK בשיתופים
def rsa_wrap_dek(dek: bytes, public_pem: bytes) -> str:
    """RSA-OAEP-encrypt a DEK under a recipient's public key."""
    if isinstance(public_pem, str):
        public_pem = public_pem.encode('utf-8')
    public_key = serialization.load_pem_public_key(public_pem)
    ciphertext = public_key.encrypt(
        dek,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )
    return base64.b64encode(ciphertext).decode('utf-8')

def rsa_unwrap_dek(wrapped_b64: str, private_pem: bytes) -> bytes:
    """Reverse rsa_wrap_dek using the recipient's private-key PEM."""
    if isinstance(private_pem, str):
        private_pem = private_pem.encode('utf-8')
    private_key = serialization.load_pem_private_key(private_pem, password=None)
    return private_key.decrypt(
        base64.b64decode(wrapped_b64),
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None,
        ),
    )

איך עובד שיתוף קובץ?

  1. אני (אליס) מעלה קובץ. נוצר DEK חדש. הקובץ מוצפן עם ה-DEK. ה-DEK נעטף ב-Master KEK ונשמר ב-Firestore.
  2. אני משתפת עם בוב. השרת מביא את המפתח הציבורי של בוב, פותח את ה-DEK עם ה-Master KEK, ועוטף אותו שוב עם המפתח הציבורי של בוב (RSA-OAEP). זה נשמר במסמך השיתוף.
  3. בוב מוריד את הקובץ. השרת לוקח את ה-DEK העטוף ב-RSA, פותח אותו עם המפתח הפרטי של בוב (גם הוא היה עטוף ב-Master KEK). עם ה-DEK הזה הוא מפענח את הקובץ.
למה RSA-OAEP ולא RSA רגיל? כי RSA "טהור" לא מאובטח מבלי padding נכון. OAEP (Optimal Asymmetric Encryption Padding) מוסיף אקראיות שמונעת מתקפות.

סיסמאות — Hash, לא הצפנה

server/models/user.py:76–87 · scrypt לסיסמאות
def set_password(self, plain_password):
    # Pin scrypt explicitly so a future Werkzeug default change cannot
    # silently weaken hashing. check_password_hash still auto-detects the
    # algorithm prefix on verify, so old pbkdf2 hashes keep working.
    self._password_hash = generate_password_hash(plain_password, method='scrypt')

def verify_password(self, plain_password):
    if not self._password_hash:
        return False
    return check_password_hash(self._password_hash, plain_password)

סיסמה אף פעם לא מוצפנת — היא מ-Hash. ההבדל: הצפנה היא דו-כיוונית (אפשר להפך), Hash הוא חד-כיווני. מנסחר אקראי (salt) מתווסף לפני ה-Hash כדי שאם יש שני משתמשים עם אותה סיסמה, ה-Hash יוצא שונה.
למה scrypt ולא MD5 או SHA-256? כי MD5 ו-SHA הם מהירים מדי — תוקף שגונב את מסד הנתונים יכול לנסות מיליארדי סיסמאות בשנייה. scrypt תוכנן להיות איטי בכוונה ולדרוש זיכרון רב — מה שמקשה על מתקפות brute-force.

JWT — אימות עם חתימה דיגיטלית

server/utils/auth.py:22–67 · אימות JWT
def _resolve_user(token):
    from routes.auth import JWT_ISSUER, JWT_AUDIENCE
    try:
        data = jwt.decode(
            token,
            app.config['SECRET_KEY'],
            algorithms=['HS256'],
            issuer=JWT_ISSUER,
            audience=JWT_AUDIENCE,
        )
    except jwt.ExpiredSignatureError:
        return None, ({"error": "Token has expired"}, 401)
    except jwt.InvalidTokenError:
        return None, ({"error": "Invalid token"}, 401)
    ...
    revoke_after = user_data.get('token_revoked_after')
    iat = data.get('iat')
    if revoke_after and iat is not None:
        try:
            iat_dt = datetime.utcfromtimestamp(int(iat))
            revoke_dt = datetime.fromisoformat(revoke_after)
            if iat_dt <= revoke_dt:
                return None, ({"error": "Token has been revoked"}, 401)
        except (ValueError, TypeError):
            return None, ({"error": "Token has been revoked"}, 401)

JWT (JSON Web Token) = "תעודה" שהשרת חתם דיגיטלית, ומכילה את שם המשתמש + זמן הוצאה + זמן תפוגה. הלקוח שולח את התעודה בכל בקשה ב-Header של Authorization. השרת מפענח, מוודא את החתימה (HS256 = HMAC-SHA256 עם SECRET_KEY), בודק שלא פג תוקף, ושלא שוללה. אם הכל בסדר — יודע מי המשתמש.
הסוד שעל פיו חותמים נטען מ-config.py:14 שמייצר אותו אקראית בפעם הראשונה ושומר ב-server/.secret_key (לא ב-git!).

טיפול בפרצות אבטחה — אילו פרצות הפרויקט פותר

פרצהמה זההפתרון בפרויקט
סוד קשיח בקודאם SECRET_KEY קשיח בקוד, כל אחד שרואה את הקוד יכול לזייף JWT.config.py:14–33 — הסוד נטען מ-env var או נוצר אקראית ונשמר בקובץ מחוץ ל-git.
CORS פתוחאם השרת מקבל בקשות מכל מקור, אתר זדוני יכול לדבר עם ה-API בשם המשתמש.config.py:48–63 — רשימת מקורות מוגדרת, ברירת מחדל localhost בלבד.
חוסר Security Headersבלי CSP, X-Frame-Options — האתר חשוף ל-Clickjacking ו-XSS.app.py:22–49 — CSP מלא, X-Frame-Options=DENY, nosniff, Referrer-Policy, HSTS על HTTPS.
IDOR (Insecure Direct Object Reference)אם אני יכולה לגשת ל-/api/files/123 של מישהו אחר רק כי אני יודעת את ה-ID.כל ניתוב בודק check_ownership(username) או uploaded_by == current_user['username'].
Zip Bomb / DoSתיקייה ענקית = הורדה אינסופית = השרת קורס מ-OOM.directory_item.py:14–15MAX_ZIP_FILES=2000, MAX_ZIP_BYTES=512MB. נזרק ZipLimitExceeded אם חורגים.
Stored XSSמשתמש מזין שם משתמש <script>alert(1)</script> ואני שם אותו ב-DOM עם innerHTML.בלקוח אנחנו משתמשים ב-textContent כמעט בכל מקום (123 שימושים) ולא ב-innerHTML לתוכן משתמש.
Brute-force סיסמאותתוקף מנסה אינסוף סיסמאות.סיסמאות עם scrypt — איטי בכוונה. ניתן להוסיף Rate Limiting בעתיד.
הצפנה במנוחהאם מישהו גונב את ה-GCS bucket, הוא יכול לקרוא קבצים.Envelope Encryption — שכבה נוספת מעל ההצפנה של גוגל.

דוגמת קוד — IDOR ו-check_ownership

server/models/storage_item.py:78–80 · בדיקת בעלות
def check_ownership(self, username):
    """True iff this item belongs to the given user."""
    return self._owner == username

זה לב הלב של מודל ההרשאות: לכל קובץ ולכל תיקייה יש שדה owner, וכל פעולה (הורדה, מחיקה, שיתוף) קוראת ל-check_ownership(current_user['username']). הנתיבים מחזירים 403 או 404 אם הבדיקה נכשלת. כך לא ניתן לגשת לקובץ של משתמש אחר רק כי יודעים את ה-ID שלו.

חולשה גלויה — אין בדיקת magic number של קבצים

הפרויקט מקבל קבצים מהמשתמש ושומר אותם ב-GCS. אנחנו לא בודקים את הבייטים הראשונים (magic number) כדי לוודא שמדובר באמת בקובץ מהסוג שהמשתמש הצהיר עליו. אם משתמש מעלה image.png שהוא בעצם image.png.exe — אנחנו לא נדע.

איך מקלים על הסיכון? אנחנו שומרים את הקובץ עם Content-Type: application/octet-stream ב-GCS, כך שכשמורידים — הדפדפן מוריד כקובץ ולא מריץ. וכן אנחנו עוטפים את השם ב-secure_filename() של Werkzeug שמסיר תווים מסוכנים.

תשובת גיבוי אם הבוחן שואל: "זה שיפור עתידי שאני מתכננת. כיום אני מסתמכת על שני עקיפי הקטנה: secure_filename מסיר תווים בעייתיים מהשם, ובאחסון אני כותבת את כל הקבצים עם content-type=application/octet-stream כדי שגם אם מישהו מצליח להעלות קובץ exe מחופש לתמונה, הדפדפן לא יריץ אותו אלא יוריד אותו. בעתיד הייתי מוסיפה ספרייה כמו python-magic שמזהה לפי 4-8 הבייטים הראשונים את הסוג האמיתי."

תשובות מוכנות לשאלות אבטחה

5. ממשק משתמש אינטראקטיבי

בפרויקט יש שני ממשקים:

ממשק Web (HTML/CSS/JS) — בדפדפן

דףתיקייהפיצ'רים אינטראקטיביים
ביתclient/home/גרירה ושחרור (drag-drop) של קבצים להעלאה, חיפוש בזמן אמת, פירורי לחם, מודלים לפעולות, סטטוס העלאה, רשימת מועדפים, שיתוף, מעבר לסל מיחזור, יצירת תיקיות.
פרופילclient/profile/עריכת פרטים, החלפת תמונה, שינוי סיסמה, ניתוק מכל המכשירים.
אדמיןclient/admin/סטטיסטיקות מערכת, רשימת משתמשים עם נפח אחסון, מחיקת משתמש.
התחברותclient/login/ולידציה בצד לקוח, הודעות שגיאה אינטראקטיביות.
הרשמהclient/signup/אישור סיסמה, ולידציה של אורך/תוים.
בכל הדפיםclient/theme.jsמתג כהה/בהיר, נשמר ב-localStorage.

ממשק Desktop (Tkinter) — אפליקציה מקומית

desktop/app.py:24–90 · המבנה של DesktopApp
class DesktopApp:
    def __init__(self, root):
        self.root = root
        self.root.title("CloudStorage - Notifications")
        self.root.geometry("560x520")

        self.api = None
        self.nc = None
        self._shared_files = []

        self._build_login_frame()
        self._build_main_frame()
        self._show_login()

    def _build_login_frame(self):
        self.login_frame = ttk.Frame(self.root, padding=20)
        ttk.Label(self.login_frame, text="Connect to CloudStorage",
                  font=("Segoe UI", 14, "bold")).grid(...)
        ...

    def _build_main_frame(self):
        self.main_frame = ttk.Frame(self.root, padding=12)
        self.status_label = ttk.Label(self.main_frame, ...)
        self.notif_text = scrolledtext.ScrolledText(self.main_frame, height=10, ...)
        ...
        self.shared_list = tk.Listbox(self.main_frame, height=8)
        ...

שני מצבים: דף התחברות ודף ראשי. אחרי התחברות מוצלחת — עוברים לדף הראשי, רואים בלוק "Live notifications" שמתעדכן בזמן אמת מהסוקט, ורשימת קבצים ששותפו איתי.

desktop/app.py:132–157 · עדכון UI מאירועי סוקט (cross-thread safe)
def _poll_events(self):
    """Drain the socket client's event queue on the UI thread."""
    if self.nc is None:
        return
    try:
        while True:
            event = self.nc.events.get_nowait()
            self._handle_event(event)
    except Exception:
        pass  # queue empty
    self.root.after(150, self._poll_events)

def _handle_event(self, event):
    etype = event.get("type")
    if etype == "shared":
        owner = event.get("owner", "someone")
        name = event.get("item_name", "an item")
        self._log(f"{owner} shared \"{name}\" with you.")
        self._refresh_shared()
        messagebox.showinfo("New share", f'{owner} shared "{name}" with you.')

בעיה: ה-thread שמקבל הודעות מהסוקט הוא לא ה-UI thread. Tkinter לא thread-safe — אם הייתי קוראת ל-messagebox.showinfo מתוך ה-thread של הסוקט, ה-UI היה קורס. פתרון: ה-thread של הסוקט שם הודעות בתור (queue.Queue), ו-root.after(150, ...) רץ ב-UI thread כל 150 מילי-שניות ושולף הודעות מהתור.

תשובות מוכנות לשאלות UI

תיאוריה: תקשורת

מודל OSI ושכבותיו

שכבהשםדוגמהאיפה בפרויקט?
7ApplicationHTTP, SMTP, DNS, FTP, הפרוטוקול שליהפרוטוקול JSON שלי על הסוקט; HTTP של Flask
6Presentationהצפנה (TLS), קידודFernet, RSA, JSON encoding
5Sessionניהול חיבורJWT, חיבור הסוקט הפתוח
4TransportTCP, UDPTCP על פורט 5000 (HTTP) ו-5001 (סוקט שלי)
3NetworkIP, ICMPIPv4 (AF_INET ב-socket.socket)
2Data LinkEthernet, MAC, ARPמנוהל ע"י מערכת ההפעלה
1Physicalחוטים, גלי רדיומתחת לראדאר שלנו

TCP מול UDP — ההבדל

היבטTCPUDP
קישורמבוסס חיבור (3-way handshake)חסר חיבור (connectionless)
אמינותמבטיח הגעה וסדרלא מבטיח כלום — אפשר לאבד, לקבל לא בסדר, לקבל פעמיים
מהירותאיטי יותר (overhead)מהיר ורזה
שימושיםHTTP, SSH, האפליקציה שליDNS, VoIP, משחקים

לחיצת יד משולשת (3-Way Handshake)

Client                            Server
  │                                  │
  │ ──── SYN (seq=x) ──────────►    │   "אני רוצה להתחבר"
  │                                  │
  │ ◄──── SYN+ACK (seq=y, ack=x+1)──│   "מסכים, גם אני"
  │                                  │
  │ ──── ACK (ack=y+1) ─────────►   │   "סבבה, התחלנו"
  │                                  │
  │       חיבור פתוח                │

שלוש הודעות = "לחיצה משולשת". זו הסיבה ש-TCP יודע שהחיבור באמת קם — שני הצדדים אישרו שהם רואים זה את זה.

IP, PORT, PING

פרוטוקולים בשכבת האפליקציה

פרוטוקולתפקידפורט סטנדרטי
HTTPתקשורת Web80 (HTTPS = 443)
SMTPשליחת אימייל25 / 587
DNSתרגום שם דומיין ל-IP53 (UDP בעיקר)
ARPתרגום IP ל-MAC ברשת מקומית (שכבה 2)
FTPהעברת קבצים21
SSHקונסולת רחוקה מאובטחת22

תיאוריה: קריפטוגרפיה

סוגי הצפנה

מפתח ציבורי מול פרטי

במערכת א-סימטרית, יש זוג מפתחות שמתואמים מתמטית. כל מה שהוצפן עם אחד ניתן לפענח רק עם השני.

חתימה דיגיטלית

שלוש פונקציות:

  1. אימות מקור — רק לי יש את המפתח הפרטי, אז רק אני יכולה לחתום.
  2. אימות שלמות — אם מישהו משנה את המסר אחרי החתימה, האימות ייכשל.
  3. אי-הכחשה — אני לא יכולה להגיד "לא אני חתמתי" אם המפתח הפרטי שלי באמת חתם.

ב-JWT שלי החתימה היא HMAC-SHA256 (חתימה סימטרית) — לא חתימה דיגיטלית אמיתית, אבל מספיק בשביל המקרה שלי, כי השרת שלי גם חותם וגם מאמת.

Hash

תיאוריה: מערכות הפעלה

Thread מול Process

היבטProcessThread
זיכרוןזיכרון נפרד לחלוטיןזיכרון משותף עם שאר ה-threads באותו process
תקשורתIPC: pipes, sockets, shared memoryמשתנים גלובליים, חפצים משותפים (צריך Lock!)
יצירהיקרה (fork/CreateProcess)זולה
קריסהprocess אחד נופל — לא משפיע על אחריםthread שמקריס סופר exception עלול להפיל את כל ה-process
אצלי בפרויקטFlask + Socket הם תהליך אחדהרבה threads בתוכו

WinAPI

WinAPI הוא ספריית הפונקציות של Windows שאפליקציות קוראות אליה בשביל לפנות למערכת ההפעלה: לפתוח קבצים, ליצור threads, לצייר חלונות וכו'. ב-Python יש לי ctypes או pywin32 לקרוא ישירות ל-WinAPI, אבל בפרויקט שלי אני לא קוראת ישירות — אני משתמשת ב-threading ו-socket מהספרייה התקנית של Python, שמתחתם משתמשים ב-WinAPI עבורי. למשל threading.Thread.start() בסופו של דבר קורא ל-CreateThread של Windows.

תיאוריה: קבצים

Magic Number

ה-magic number הם הבייטים הראשונים של קובץ שמזהים את הסוג שלו, ללא תלות בסיומת השם. דוגמאות:

סוג קובץבייטים ראשונים (Hex)
PNG89 50 4E 47 0D 0A 1A 0A
JPEGFF D8 FF
PDF25 50 44 46 (%PDF)
ZIP / docx / xlsx / jar50 4B 03 04 (PK..)
PE (Windows EXE/DLL)4D 5A (MZ)
ELF (Linux)7F 45 4C 46 (.ELF)

למה זה רלוונטי לפרויקט? כי משתמש יכול לשנות שם קובץ מ-evil.exe ל-evil.png, והשרת לא יבדל בלי בדיקת ה-magic. אצלי: אני לא בודקת magic, רק מגנה דרך content-type=octet-stream באחסון (ראה הקופסה הכתומה בסעיף 4).

FAT32

מערכת קבצים ישנה של Windows ומכשירי USB:

PE — Portable Executable

פורמט קבצי הרצה של Windows: .exe, .dll, .sys.

תיאוריה: סייבר — מתקפות והגנות

MITM — Man-In-The-Middle

תוקף נכנס בין הלקוח לשרת ומקריא/משנה את התקשורת. אצלי:

SQL Injection

תוקף מזריק קוד SQL לתוך input של משתמש. למשל:

SELECT * FROM users WHERE username = '' OR '1'='1';

אצלי: הסיכון לא רלוונטי — אני משתמשת ב-Firestore (NoSQL) ולא ב-SQL. בנוסף, ספריית Firebase Admin מקבלת ערכים כפרמטרים (לא קונקטנציה של מחרוזות) — מה שמקביל ל-prepared statements.

תשובה לבוחן: "הפרויקט שלי לא חשוף ל-SQL Injection כי הוא לא משתמש ב-SQL. הוא משתמש ב-Firestore — NoSQL. הסיכון המקביל ב-NoSQL הוא NoSQL Injection, אבל ה-SDK של Firebase Admin מעביר ערכים כפרמטרים, לא בונה queries ע"י קונקטנציה של מחרוזות, אז גם הוא לא חשוף."

Buffer Overflow

תוקף שולח קלט גדול יותר מהבאפר שמוקצה לו, וכותב על אזורים אחרים בזיכרון. בעיקרון מאפשר הרצת קוד שרירותי.

אצלי: לא חשוף — Python מנהל זיכרון בעצמו (managed memory). list ו-str ב-Python גדלים דינמית — אין גלישה. ה-buffer overflow רלוונטי לשפות עם ניהול זיכרון ידני: C, C++, Assembly.

תשובה לבוחן: "הפרויקט שלי לא חשוף ל-buffer overflow כי Python היא שפה עם ניהול זיכרון אוטומטי. הסיכון רלוונטי ל-C/C++ עם strcpy, gets ופונקציות שלא בודקות את גודל הקלט."

XSS — Cross-Site Scripting

תוקף מזריק קוד JavaScript לדף שלי. למשל אם משתמש מזין כשם משתמש <script>alert(document.cookie)</script> ואני מציגה את זה ב-DOM עם innerHTML — הסקריפט ירוץ אצל כל מי שצופה.

אצלי: כמעט בכל מקום אני משתמשת ב-textContent ולא ב-innerHTML — וזה מחליף את התווים < ו-> בייצוג טקסטואלי. דוגמה: client/profile/profile.js:13 משתמש ב-textContent להצגת שם משתמש. בנוסף, ה-CSP header חוסם הרצת scripts inline ממקורות זרים.

סוגי מתקפות נוספות — לידע כללי

חלק ג' — מבחן מדומה (35 שאלות)

תשובות קצרות בעברית, מסודרות מקל לקשה. שאלות 1–10 בסיסיות, 11–20 בינוניות, 21–35 קשות. כשמופיע 📍 פתחי... זה רמז לקובץ שאת צריכה לפתוח אם הבוחן ידרוש "תראי לי בקוד".

1. כמה מחלקות יש בפרויקט שלך?
לפחות 9 מחלקות שאני כתבתי: StorageItem, FileItem, DirectoryItem, User, AdminUser, NotificationHub, NotificationClient, ApiClient, DesktopApp.
📍 server/models/, server/socket_server.py, desktop/
2. תני דוגמה להורשה.
FileItem יורש מ-StorageItem. ב-file_item.py:17 כתוב class FileItem(StorageItem):. הוא מקבל בחינם את check_ownership, soft_delete, ועוד.
📍 server/models/file_item.py:17
3. מה זה פולימורפיזם בפרויקט שלך?
המתודה soft_delete ב-StorageItem קוראת ל-self.item_type(). אצל FileItem היא מחזירה 'file', אצל DirectoryItem היא מחזירה 'directory'. אותו קוד, התנהגות שונה — לפי הסוג בזמן ריצה.
📍 server/models/storage_item.py:87
4. מה זה כימוס?
לדאוג ששדות פנימיים של מחלקה לא נגישים מבחוץ. אצלי כל המשתנים מתחילים ב-underscore (_username, _password_hash), וגישה מבחוץ דרך @property. הסיסמה רק דרך set_password כדי להבטיח hashing.
📍 server/models/user.py:22
5. איזה פרוטוקול תקשורת את משתמשת? למה?
TCP על שני ערוצים: HTTP על פורט 5000 (Flask), וסוקט TCP גולמי על פורט 5001. בחרתי TCP כי אני שולחת הודעות מובנות שחייבות להגיע בסדר ובאמינות. עם UDP הייתי יכולה לאבד הודעות שיתוף.
📍 server/socket_server.py:84 (SOCK_STREAM)
6. מה זו לחיצת יד משולשת?
תהליך הקמת חיבור TCP בן 3 הודעות: הלקוח שולח SYN, השרת מחזיר SYN+ACK, הלקוח שולח ACK. רק אז החיבור נחשב פתוח.
7. מה ההבדל בין TCP ל-UDP?
TCP מבוסס חיבור, מבטיח הגעה וסדר, איטי יותר. UDP חסר חיבור, לא מבטיח כלום, מהיר. TCP ל-HTTP/SSH. UDP ל-DNS/VoIP/משחקים.
8. מה זה IP ומה זה PORT?
IP מזהה מכונה ברשת (192.168.1.7 או 127.0.0.1). PORT מזהה תהליך בתוך המכונה (5000 ל-Flask שלי, 5001 לסוקט שלי). יחד הם מזהים יעד מדויק.
9. מה זה JWT?
JSON Web Token — תעודה שהשרת חתם עליה דיגיטלית. הלקוח שולח אותה בכל בקשה. השרת מאמת חתימה, בודק תפוגה, ויודע מי המשתמש. אצלי החתימה היא HS256 (HMAC-SHA256).
📍 server/utils/auth.py:32
10. מה ההבדל בין hash להצפנה?
הצפנה דו-כיוונית — אפשר לפענח. Hash חד-כיוונית — אי אפשר. סיסמאות תמיד hash (אסור שהשרת יוכל לקרוא אותן). אצלי scrypt לסיסמאות.
📍 server/models/user.py:80
11. איך השרת שלך תומך במספר לקוחות?
Thread לכל לקוח. ה-accept loop רץ בthread נפרד; לכל חיבור חדש נוצר thread דאמון שמטפל באותו לקוח. ככה accept מיד פנוי ללקוח הבא.
📍 server/socket_server.py:94
12. הסבירי את פרוטוקול ההודעות שלך.
JSON מסיים ב-newline על TCP. ההודעה הראשונה מהלקוח חייבת להיות {"type":"auth","token":...}. אחר כך ping/pong. השרת דוחף הודעות כמו {"type":"shared",...} כשמישהו משתף איתי קובץ.
📍 server/socket_server.py:1–23 (docstring)
13. למה JSON ולא פרוטוקול בינארי?
קל לקריאה ולדיבוג, נתמך מובנה ב-Python ובJavaScript, גודל המסר שלי קטן (פחות מקילובייט) אז ה-overhead לא משמעותי. אם הייתי שולחת בייטים גולמיים של קובץ — הייתי שוקלת פרוטוקול בינארי.
14. איך את מפרידה בין הודעות בסטרים של TCP?
פריימינג לפי \n. יש לי באפר שמצטבר; כשמופיע \n אני חותכת את ההודעה. _LineReader ב-socket_server.py:146.
15. מה את עושה כשלקוח מתנתק פתאום?
recv מחזיר 0 בייטים. ה-thread יוצא מהלולאה ומבצע cleanup ב-finally: מסיר את הלקוח מטבלת החיבורים וסוגר את הסוקט. השרת ממשיך לעבוד.
📍 server/socket_server.py:127–143
16. למה את צריכה Lock בקוד?
כי טבלת _connections נכתבת ונקראת מהרבה threads בו-זמנית. בלי Lock אפשר לקבל race condition — שני threads ישנו את אותו dict באותו זמן והנתונים יהיו שבורים.
📍 server/socket_server.py:44
17. מה זה Thread ומה זה Process?
Process זה תוכנית רצה — זיכרון נפרד, אבטחה נפרדת. Thread זה "חוט ביצוע" בתוך process — חולק זיכרון עם שאר ה-threads. ב-Python: multiprocessing ל-processes, threading ל-threads.
18. למה את משתמשת ב-ThreadPoolExecutor ולא ב-Thread רגיל?
כי אני רוצה לחסום את מספר ה-threads המקסימלי. אם מישהו מוריד תיקייה עם 1000 קבצים, אני לא רוצה ליצור 1000 threads. ThreadPoolExecutor(max_workers=8) מבטיח 8 במקביל בלבד.
📍 server/models/directory_item.py:280
19. מה זה הצפנה סימטרית מול א-סימטרית?
סימטרית: אותו מפתח להצפנה ופענוח (AES, Fernet). מהירה. א-סימטרית: שני מפתחות (RSA). איטית, פותרת את בעיית החלפת המפתחות. אצלי שילוב: DEK סימטרי לקובץ, RSA לעטיפת ה-DEK בשיתוף.
20. מה זה Envelope Encryption?
דפוס של הצפנה כפולה: מפתח DEK פר-קובץ מצפין את הקובץ, ומפתח Master KEK מצפין את ה-DEK. למה? כי כדי לסבב את ה-KEK אני לא צריכה לפענח ולהצפין מחדש את כל הקבצים — רק לעטוף מחדש את ה-DEKs הקטנים.
📍 server/utils/crypto.py:1–36
21. הסבירי את תהליך השיתוף המוצפן בפרויקט שלך.
למעלה אצלי בקובץ. בקצרה: כשאני (אליס) משתפת קובץ עם בוב, השרת לוקח את ה-DEK של הקובץ, פותח אותו עם Master KEK, ועוטף אותו מחדש עם המפתח הציבורי של בוב (RSA-OAEP). בוב מוריד — השרת פותח את ה-DEK עם המפתח הפרטי של בוב, ומפענח את הקובץ.
📍 server/utils/crypto.py:152–212
22. למה RSA-OAEP ולא RSA רגיל?
RSA "טקסטבוק" לא מאובטח — חשוף למתקפות. OAEP (Optimal Asymmetric Encryption Padding) מוסיף padding מקרי שמונע מתקפות הצפנה דטרמיניסטית.
23. למה scrypt ולא MD5 לסיסמאות?
MD5 ו-SHA הם מהירים בכוונה — תוקף שגונב את ה-DB יכול לנסות מיליארדי סיסמאות בשנייה (rainbow tables). scrypt תוכנן להיות איטי ולדרוש זיכרון רב — מקשה על brute force פי מיליון.
24. מה זו חתימה דיגיטלית? יש לך אחת בפרויקט?
חתימה עם המפתח הפרטי, אימות עם הציבורי. מבטיחה זהות + שלמות + אי-הכחשה. ב-JWT שלי החתימה היא HMAC-SHA256 — חתימה סימטרית (אותו מפתח חותם ומאמת), ולא חתימה דיגיטלית אמיתית. בשיתופים אני משתמשת ב-RSA להצפנה, לא לחתימה.
25. איך הפרויקט שלך מוגן מ-MITM?
HTTPS ל-Flask (כשמופעל עם dev cert), JWT חתום ולא ניתן לזיוף, כל הסיסמאות מועברות רק על HTTPS. החולשה: הסוקט שלי TCP גלוי, אבל הוא רץ על localhost בלבד. שיפור עתידי: ssl.wrap_socket.
26. איך הפרויקט שלך מוגן מ-SQL Injection?
לא רלוונטי — אני משתמשת ב-Firestore, NoSQL. בנוסף ה-SDK מקבל ערכים כפרמטרים, לא בקונקטנציה. סיכון מקביל ב-NoSQL היה NoSQLi, אבל גם הוא לא רלוונטי כי אני לא בונה queries ידנית.
27. איך הפרויקט שלך מוגן מ-Buffer Overflow?
Python היא שפה עם ניהול זיכרון אוטומטי — אין באפרים סטטיים שאני מקצה ידנית. הסיכון רלוונטי ל-C/C++.
28. איך הפרויקט מוגן מ-XSS?
בלקוח אני משתמשת ב-textContent ולא ב-innerHTML להצגת תוכן משתמש (123 מופעים על פני 6 קבצים). בנוסף יש CSP header שחוסם scripts inline ממקורות זרים.
📍 client/profile/profile.js:13, client/admin/admin.js:15
29. איך הפרויקט שלך מוגן מ-IDOR?
לכל קובץ ולכל תיקייה יש owner ב-DB. כל פעולה בודקת check_ownership(current_user['username']) או משווה uploaded_by. גם אם משתמש יודע ID של קובץ של מישהו אחר — הוא יקבל 403/404.
📍 server/models/storage_item.py:78
30. איך הפרויקט שלך מוגן מ-DoS דרך Zip?
תיקייה ענקית = הורדת זיפ אינסופית = השרת קורס. אני הגדרתי MAX_ZIP_FILES=2000 ו-MAX_ZIP_BYTES=512MB ב-directory_item.py:14. ברגע שעוברים — חריגה ZipLimitExceeded.
31. איפה את שומרת את הקבצים? איך משחזרים אותם?
המטא-דאטה ב-Firestore (NoSQL): collections של users, files, directories, shares, favorites, recycle_bin. הבייטים של הקבצים ב-Google Cloud Storage (אובייקטים בענן). המסלול: users/{username}/files/{timestamp}_{filename}. בהורדה — שולפים מטא-דאטה מ-Firestore, מורידים בייטים מ-GCS, מפענחים, שולחים ללקוח.
📍 server/config.py:65–82, server/models/file_item.py:115
32. מה זה Magic Number של קובץ?
הבייטים הראשונים שמזהים את סוג הקובץ ללא תלות בסיומת. PNG מתחיל ב-89 50 4E 47, PDF מתחיל ב-%PDF, EXE של Windows ב-MZ. אצלי לא ממומש — שיפור עתידי.
33. מה זה FAT32?
מערכת קבצים ישנה, נמצאת היום בעיקר ב-USB. מקסימום 4GB לקובץ, 2TB למחיצה. בלי הרשאות ובלי journal. Windows מודרני משתמש ב-NTFS, Linux ב-ext4.
34. מה זה קובץ PE?
Portable Executable — פורמט קבצי הרצה של Windows (exe, dll, sys). מתחיל ב-MZ header ואז PE header. מכיל סקציות (קוד, נתונים, משאבים) וטבלת imports/exports. ב-Linux המקביל זה ELF.
35. תני תרחיש סוף-לסוף של שיתוף קובץ.
  1. אליס מתחברת בדפדפן (POST /api/auth/login) — מקבלת JWT.
  2. אליס מעלה קובץ.docx. השרת יוצר DEK חדש, מצפין את הבייטים עם Fernet, עוטף את ה-DEK ב-Master KEK, שומר את ה-ciphertext ב-GCS, ואת המטא-דאטה (כולל wrapped_dek) ב-Firestore.
  3. בוב מפעיל את אפליקציית ה-Desktop. ApiClient מתחבר ל-Flask, מקבל JWT. אז NotificationClient פותח סוקט ל-5001 ושולח {"type":"auth","token":...}. השרת בודק, מאשר, רושם בטבלת החיבורים.
  4. אליס משתפת את הקובץ.docx עם בוב. Flask route ב-sharing.py: לוקח את ה-DEK של הקובץ, פותח עם Master KEK, עוטף עם המפתח הציבורי של בוב ב-RSA-OAEP, שומר ב-Firestore. אז קורא ל-notification_hub.push_to_user("bob", {"type":"shared",...}).
  5. השרת שולח את ההודעה דרך הסוקט הפתוח של בוב. NotificationClient של בוב מקבל, שם ב-queue. ה-UI thread של Tkinter מאסס, מציג messagebox "Alice shared X with you".
  6. בוב לוחץ Download. ApiClient עושה GET /api/shared-files/{id}/download. השרת מבקש את ה-wrapped_dek של בוב, פותח אותו עם המפתח הפרטי של בוב (שגם הוא עטוף ב-Master KEK), מפענח את הקובץ, מחזיר את הבייטים. ApiClient כותב ל-disk עם filedialog.asksaveasfilename.
📍 הקובץ הכי טוב להתחיל להראות: server/routes/sharing.py

חלק ד' — תרגילי ניווט בקוד

הבוחן יבקש "תראי לי בקוד איפה...". זה תרגיל חזרה: אם את מצליחה לפתוח את הקובץ ולהצביע על השורה תוך 10 שניות, את מוכנה.

השאלהפתחי
"תראי לי דוגמה למחלקה מופשטת"server/models/storage_item.py:20
"תראי לי הורשה"server/models/file_item.py:17 או user.py:207
"תראי לי פולימורפיזם"server/models/storage_item.py:87 (soft_delete)
"תראי לי איפה את יוצרת socket"server/socket_server.py:84
"תראי לי איפה את עושה bind/listen/accept"server/socket_server.py:86–97
"תראי לי איפה את יוצרת thread לכל לקוח"server/socket_server.py:100
"תראי לי את הפרוטוקול שלך"server/socket_server.py:1–23 (docstring)
"תראי לי הצפנה סימטרית"server/utils/crypto.py:101–113
"תראי לי הצפנה א-סימטרית"server/utils/crypto.py:182–212
"תראי לי hash של סיסמה"server/models/user.py:80
"תראי לי איפה את בודקת JWT"server/utils/auth.py:31
"תראי לי security headers"server/app.py:22–49
"תראי לי בדיקת בעלות (IDOR)"server/models/storage_item.py:78
"תראי לי הגנה מ-Zip bomb"server/models/directory_item.py:14, 259
"תראי לי ThreadPoolExecutor"server/models/directory_item.py:280
"תראי לי איפה ה-UI מקבל הודעות מהסוקט"desktop/app.py:132 (_poll_events)
"תראי לי איפה את כותבת קובץ ל-disk"desktop/api_client.py:82
"תראי לי שימוש ב-Lock"server/socket_server.py:49
"תראי לי איפה הסוקט שולח הודעה ללקוח"server/socket_server.py:60 (push_to_user)
"תראי לי איפה route של Flask קורא לסוקט"server/routes/sharing.py:130

דף עזר אחרון — לפני שנכנסים לבחינה

הוכחות 30 שניות — אם הבוחן רוצה שכל דבר תיפול בקלות

שלוש "חסר" שצריך להכין תשובה מראש

  1. אין TLS על הסוקט. "TCP גלוי, אבל רץ על localhost. שיפור עתידי: ssl.wrap_socket."
  2. אין בדיקת magic number על העלאה. "מקלים דרך content-type=octet-stream באחסון ו-secure_filename. שיפור: ספריית python-magic."
  3. אין WinAPI/COM/חומרה. "המחוון מציע 'או/או'. אני מקיימת גישה למערכת קבצים + API."

בהצלחה!

הפרויקט שלך עומד במלוא הדרישות — יותר מהרגיל. אם הבוחן ישאל משהו שאת לא יודעת, אל תמציאי: "זה נושא שאני פחות חזקה בו" עדיף בהרבה מתשובה לא נכונה. הבוחן אוהב כנות וביטחון.

זכרי: תהליך שלם מקצה לקצה שווה 30%. שליטה בקוד שווה 30%. אם את יודעת לפתוח את המערכת, להעלות קובץ, לשתף, לראות התראה ב-Desktop ולהוריד — את כבר ב-30% הראשונים. אם את יודעת לנווט לקוד בלי לחפש — עוד 30%. השאר זה תיאוריה — והמסמך הזה הוא בדיוק זה.