קריפטוגרפיה
הצפנה, קידוד וגיבוב: מה כל אחד מהם עושה
הפרדה בין קידוד, הצפנה וגיבוב במעבדות עם Base64, SHA-256 ו-OpenSSL. למה Base64 אינו הגנה ולמה hash הוא חד־כיווני.
בונים צופן קיסר ב-Python, בודקים אותו עם unittest ורואים למה צופן הזזה כזה לא נותן סודיות אמיתית.
כותבים פונקציה בשם caesar(). היא מזיזה אותיות באנגלית במספר נתון של מיקומים, לא נוגעת בשאר התווים והופכת את ההזזה כשמעבירים לה מספר שלילי. אחר כך מריצים חבילת בדיקות קטנה וקוראים מה היא בודקת.
צריך Python 3 וזהו. אם הרעיון של צופן הזזה חדש, כדאי לקרוא קודם את מהו צופן קיסר?.
מתקינים Python 3 מ-python.org. הקוד משתמש רק בספרייה הסטנדרטית, כך שאין מה להתקין עם pip. יוצרים תיקייה למעבדה ושומרים בה את הקבצים.
הפלט במאמר הזה הגיע מ-Python 3.12.10 ב-Windows 11. לא בדקתי גרסאות אחרות.
הפונקציה ord() של Python מחזירה את נקודת הקוד ב-Unicode של תו. chr() עושה את ההפך והופכת נקודת קוד בחזרה לתו. עבור האות הגדולה A, נקודת הקוד היא 65.
מריצים את זה בטרמינל:
python -I -c "print(ord('A'), chr(65))"פלט:
65 Aהדגל -I מריץ את Python במצב מבודד. משתני סביבה שמתחילים במחרוזת PYTHON לא נלקחים בחשבון, והתיקייה הנוכחית לא מתווספת לנתיב החיפוש של המודולים. כדאי להשתמש בו כשמריצים קוד מתיקייה שלא כתבת בעצמך.
הזזה עובדת על מיקום שמתחיל מאפס. חיסור של 65 הופך את A ל-0 ואת Z ל-25. השלב % 26 מחזיר את התוצאה לתוך האלפבית, והוספה של 65 מחזירה לטווח האותיות הגדולות:
chr((ord(letter) - 65 + shift) % 26 + 65)אותיות קטנות מתחילות ב-97, ולכן אותו רעיון משתמש ב-97 במקום ב-65.
האופרטור % של Python אף פעם לא מחזיר מספר שלילי כשהמחלק חיובי. בגלל זה הזזה שלילית עובדת בלי קוד נוסף:
python -I -c "print((0 - 3) % 26)"פלט:
23אפס פחות שלוש חוזר ל-23, המיקום של X.
שומרים את זה בשם caesar_cipher.py. זה אותו קוד כמו בקובץ המעבדה.
"""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.
מריצים את הקובץ:
python -I caesar_cipher.pyפלט:
KHOOR, Zruog!
HELLO, World!הפונקציה משנה רק את האותיות A עד Z ו-a עד z. ספרות, רווחים, סימני פיסוק ואותיות עם סימנים דיאקריטיים עוברים בלי שינוי. כך הקוד נשאר קצר. צופן שמטפל באותיות כאלה היה צריך כלל לכל אחת מהן, והמעבדה הזאת לא מגדירה כלל כזה.
המעבדה כוללת גם את test_caesar_cipher.py. פותחים טרמינל בתיקייה שמחזיקה את שני הקבצים ומריצים:
python -I -m unittest discover -p 'test_*.py' -vפלט:
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 ומצפה לקבל חזרה את המקור. טקסט הדוגמה כולל פיסוק, ספרה ואת á, ולכן היא בודקת גם שתווים שאינם אותיות נשמרים.אם משנים את הקוד ובדיקה נכשלת, הודעת הכישלון מראה את הקלט, את הערך הצפוי ואת הערך שהתקבל. כדאי לקרוא את ההודעה הזאת לפני ששינוי אחר נכנס לקוד.
לצופן קיסר יש רק 25 הזזות שימושיות. הזזה 0 והזזה 26 מחזירות שתיהן את הטקסט המקורי. אדם יכול לנסות כל הזזה ביד, ותוכנה מנסה את כולן ברגע.
ניתוח שכיחויות הוא בעיה שנייה. באנגלית אותיות כמו E ו-T מופיעות יותר מ-Q או Z. בהודעה ארוכה יותר, האותיות הנפוצות בטקסט המוצפן מצביעות לעיתים קרובות על האותיות הנפוצות בטקסט הגלוי.
כך שהצופן הזה לא נותן סודיות אמיתית. אין להשתמש בו לסיסמאות, לרשומות לקוחות, לטוקנים או לכל דבר אחר שחשוב. כשצריך הצפנה אמיתית, משתמשים בספרייה בדוקה ובאלגוריתם סטנדרטי. OWASP Cryptographic Storage Cheat Sheet מפרט את הכללים הבסיסיים להגנה על נתונים שמורים.
את התרגיל הזה כדאי לראות כדרך ללמוד חשבון מודולו, קודי תווים ואת ההבדל בין טרנספורמציית צעצוע לצופן אמיתי.
אחרי שהבדיקות עוברות, אפשר לבקש ממודל שפה להסביר את שורת המודולו, להציע מקרי קצה או לכתוב בדיקות נוספות. פרומפט שימושי מציין את המגבלות כבר בהתחלה:
לסקור את צופן קיסר הלימודי הזה. מה קורה בהזזות שליליות, בתווים עם סימנים דיאקריטיים ובטקסט שאינו לטיני? לא לתאר אותו כהצפנה בטוחה.
כל בדיקה שהמודל מציע צריך להריץ לפני שמקבלים אותה. מודלים טועים, ובדיקה שנכשלת מלמדת יותר מהסבר בטוח בעצמו.
כשהמעבדה עובדת, כדאי לשמור את התיקייה בניהול גרסאות כדי לעקוב אחרי שינויים. המאמר הבא, מה זה GitHub?, מסביר איך לעשות את זה.
קריפטוגרפיה
הפרדה בין קידוד, הצפנה וגיבוב במעבדות עם Base64, SHA-256 ו-OpenSSL. למה Base64 אינו הגנה ולמה hash הוא חד־כיווני.
קריפטוגרפיה
למה hashes מהירים נכשלים בסיסמאות, מה salt ו-work factor עושים, ואיך מחשבים ומאמתים Argon2id ב-Python עם argon2-cffi.
קריפטוגרפיה
מפתחות ציבוריים ופרטיים מאפס: מה חתימה מוכיחה, למה המפתח הפרטי נשאר סודי, ומעבדת Ed25519 שבה קובץ ששונה נכשל באימות.