דילוג לתוכן
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

צופן קיסר ב-Python: הצפנה ופענוח של טקסט שלב אחר שלב

בונים צופן קיסר ב-Python, בודקים אותו עם unittest ורואים למה צופן הזזה כזה לא נותן סודיות אמיתית.

מתחילים4 דקות קריאהמאת NotHoly

מה בונים

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

צריך Python 3 וזהו. אם הרעיון של צופן הזזה חדש, כדאי לקרוא קודם את מהו צופן קיסר?.

לפני שמתחילים

מתקינים Python 3 מ-python.org. הקוד משתמש רק בספרייה הסטנדרטית, כך שאין מה להתקין עם pip. יוצרים תיקייה למעבדה ושומרים בה את הקבצים.

הפלט במאמר הזה הגיע מ-Python 3.12.10 ב-Windows 11. לא בדקתי גרסאות אחרות.

שלב 1: אותיות כמספרים

הפונקציה ord() של Python מחזירה את נקודת הקוד ב-Unicode של תו. chr() עושה את ההפך והופכת נקודת קוד בחזרה לתו. עבור האות הגדולה A, נקודת הקוד היא 65.

מריצים את זה בטרמינל:

bash
python -I -c "print(ord('A'), chr(65))"

פלט:

text
65 A

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

הזזה עובדת על מיקום שמתחיל מאפס. חיסור של 65 הופך את A ל-0 ואת Z ל-25. השלב % 26 מחזיר את התוצאה לתוך האלפבית, והוספה של 65 מחזירה לטווח האותיות הגדולות:

python
chr((ord(letter) - 65 + shift) % 26 + 65)

אותיות קטנות מתחילות ב-97, ולכן אותו רעיון משתמש ב-97 במקום ב-65.

האופרטור % של Python אף פעם לא מחזיר מספר שלילי כשהמחלק חיובי. בגלל זה הזזה שלילית עובדת בלי קוד נוסף:

bash
python -I -c "print((0 - 3) % 26)"

פלט:

text
23

אפס פחות שלוש חוזר ל-23, המיקום של X.

שלב 2: כתיבת הצופן

שומרים את זה בשם caesar_cipher.py. זה אותו קוד כמו בקובץ המעבדה.

python
"""Educational Caesar cipher only. NOT secure encryption.

Shift ASCII English A-Z/a-z; preserve every other Unicode character as-is.
"""

def caesar(text: str, shift: int) -> str:
    shift %= 26
    result = []
    for character in text:
        if 'A' <= character <= 'Z':
            result.append(chr((ord(character) - 65 + shift) % 26 + 65))
        elif 'a' <= character <= 'z':
            result.append(chr((ord(character) - 97 + shift) % 26 + 97))
        else:
            result.append(character)
    return ''.join(result)

if __name__ == '__main__':
    print(caesar('HELLO, World!', 3))  # KHOOR, Zruog!
    print(caesar('KHOOR, Zruog!', -3)) # HELLO, World!

השורה הראשונה, shift %= 26, מצמצמת כל הזזה לטווח 0 עד 25. הזזה של 29 מתנהגת כמו הזזה של 3, והזזה של -3 הופכת ל-23.

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

bash
python -I caesar_cipher.py

פלט:

text
KHOOR, Zruog!
HELLO, World!

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

שלב 3: הרצת הבדיקות

המעבדה כוללת גם את test_caesar_cipher.py. פותחים טרמינל בתיקייה שמחזיקה את שני הקבצים ומריצים:

bash
python -I -m unittest discover -p 'test_*.py' -v

פלט:

text
test_large_shift (test_caesar_cipher.CaesarCipherTest.test_large_shift) ... ok
test_lower (test_caesar_cipher.CaesarCipherTest.test_lower) ... ok
test_negative (test_caesar_cipher.CaesarCipherTest.test_negative) ... ok
test_roundtrip (test_caesar_cipher.CaesarCipherTest.test_roundtrip) ... ok
test_upper (test_caesar_cipher.CaesarCipherTest.test_upper) ... ok
test_wrap (test_caesar_cipher.CaesarCipherTest.test_wrap) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

כל בדיקה בודקת התנהגות אחת:

  • test_upper וגם test_lower מאשרות את התוצאות: מתוך HELLO מתקבל KHOOR, ומתוך attack מתקבל dwwdfn.
  • test_wrap מאשרת שהאות Z מתקדמת לאות C, וזה עובד רק אם החזרה להתחלה נכונה.
  • test_large_shift מאשרת שהזזה של 29 נותנת את אותה תוצאה כמו הזזה של 3.
  • test_negative בודקת פענוח: מזיזה את DEF ב-‎-3.
  • test_roundtrip מצפינה טקסט בהזזה של 7, מפענחת אותו ב-‎-7 ומצפה לקבל חזרה את המקור. טקסט הדוגמה כולל פיסוק, ספרה ואת á, ולכן היא בודקת גם שתווים שאינם אותיות נשמרים.

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

שלב 4: הבנת המגבלה

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

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

כך שהצופן הזה לא נותן סודיות אמיתית. אין להשתמש בו לסיסמאות, לרשומות לקוחות, לטוקנים או לכל דבר אחר שחשוב. כשצריך הצפנה אמיתית, משתמשים בספרייה בדוקה ובאלגוריתם סטנדרטי. OWASP Cryptographic Storage Cheat Sheet מפרט את הכללים הבסיסיים להגנה על נתונים שמורים.

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

רשות: סקירת הקוד עם מודל AI

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

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

כל בדיקה שהמודל מציע צריך להריץ לפני שמקבלים אותה. מודלים טועים, ובדיקה שנכשלת מלמדת יותר מהסבר בטוח בעצמו.

השלב הבא

כשהמעבדה עובדת, כדאי לשמור את התיקייה בניהול גרסאות כדי לעקוב אחרי שינויים. המאמר הבא, מה זה GitHub?, מסביר איך לעשות את זה.

מקורות

מקורות

  1. Python built-in functions (ord, chr)
  2. Python unittest documentation
  3. Python command-line options (-I)
  4. OWASP Cryptographic Storage Cheat Sheet

עוד בנושא קריפטוגרפיה

קריפטוגרפיה

סיסמאות ו-hashing עם Argon2id

למה hashes מהירים נכשלים בסיסמאות, מה salt ו-work factor עושים, ואיך מחשבים ומאמתים Argon2id ב-Python עם argon2-cffi.

ביניים8 דקות קריאה