היום (16 לספטמבר) ממש, אני מעביר הרצאה ב-PyconIL 2024 על בעיות קריפטוגרפיות באפליקציות פייתון. לצערי אי אפשר להכניס את כל הבעיות הקריפטוגרפיות להרצאה אחת או אפילו שלוש, אז חשבתי לכתוב פה על כמה בעיות שאני רואה בדרך כלל במימושים של פונקציות קריפטוגרפיות ובדגש על הצפנה בפייתון.
כאשר אנו מבצעים הצפנה של Data at rest או Data in transit, לא מעט פעמים אנחנו משתמשים בפונקציות הדפולטיביות של הענן או SAAS זה או אחר. אבל לא מעט פעמים, אנו כן משתמשים בתשתית קריפטוגרפית שמישהו כתב לפני שנות דור או צריכים לכתוב אחת בעצמנו. במקרים האלו, מומלץ מאד להעזר בקריפטוגרף שיוכל להסביר מה התקנים הקריפטוגרפיים המומלצים לצרכים שלכם. זו אולי המסקנה העיקרית מהפוסט הזה שיכולה לחסוך לכם את הקריאה שבגדול מראה כמה קשה לממש קריפטוגרפיה ותשתית קריפטוגרפית משל עצמכם.
רשימה של תקנים קריפטוגרפיים מומלצים נמצאת באתר של NIST וגם באתר של מיקרוסופט באופן יותר ברור וקל לקריאה לדעתי. חשוב לבחור אלגוריתם שמצד אחד מספיק חזק למה שאתם צריכים ומהצד השני לא יותר דורש משאבים כי בסקייל גדול הסיפור הזה יכול להיות יקר. מהצד השני, בחירה של אלגוריתם לא מתאים שאחר כך צריך אותו לתקינה זו או אחרת עבור לקוח חשוב יכולה לעשות נזק רב.
גם אם בוחרים את האלגוריתם הנכון והמתאים, יש עדיין כמה בעיות שצריך להיות מודעים אליהם ובמיוחד בעולם הקוד שמיוצר על ידי LLM. מעבר לעניין ה-FIPS והספריות המתאימות – לא מעט פעמים אנו בוחרים באלגוריתם, מחפשים דוגמת קוד או מייצרים כזו עם הצ׳אט ג׳יפיטי הקרוב למקום מגורינו – עושים Encrypt ו-Decrypt והכל עובד ומעולה. אבל בפועל אנו מקבלים הצפנה פחות טובה.
אני אדגים למשל עם הקוד הזה:
from Crypto.Cipher import AES
key = b'\x2b\x7e\x15\x16\x28\xae\xd2\xa6\xab\xf7\xcf\x93\x96\x7f\x52\x3a'
iv = b'1234567890123456'
def encrypt(plaintext, key, iv):
cipher = AES.new(key.ljust(16, b'from Crypto.Cipher import AES
key = b'\x2b\x7e\x15\x16\x28\xae\xd2\xa6\xab\xf7\xcf\x93\x96\x7f\x52\x3a'
iv = b'1234567890123456'
def encrypt(plaintext, key, iv):
cipher = AES.new(key.ljust(16, b'\0'), AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext) # ללא padding
return ciphertext
def decrypt(ciphertext, key, iv):
cipher = AES.new(key.ljust(16, b'\0'), AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
return plaintext
# שימוש בדוגמה
plaintext = b"1234567890123456"
ciphertext = encrypt(plaintext, key, iv)
print(f"Ciphertext: {ciphertext}")
decrypted_plaintext = decrypt(ciphertext, key, iv)
print(f"Decrypted plaintext: {decrypted_plaintext}")
'), AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext) # ללא padding
return ciphertext
def decrypt(ciphertext, key, iv):
cipher = AES.new(key.ljust(16, b'from Crypto.Cipher import AES
key = b'\x2b\x7e\x15\x16\x28\xae\xd2\xa6\xab\xf7\xcf\x93\x96\x7f\x52\x3a'
iv = b'1234567890123456'
def encrypt(plaintext, key, iv):
cipher = AES.new(key.ljust(16, b'\0'), AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(plaintext) # ללא padding
return ciphertext
def decrypt(ciphertext, key, iv):
cipher = AES.new(key.ljust(16, b'\0'), AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
return plaintext
# שימוש בדוגמה
plaintext = b"1234567890123456"
ciphertext = encrypt(plaintext, key, iv)
print(f"Ciphertext: {ciphertext}")
decrypted_plaintext = decrypt(ciphertext, key, iv)
print(f"Decrypted plaintext: {decrypted_plaintext}")
'), AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
return plaintext
# שימוש בדוגמה
plaintext = b"1234567890123456"
ciphertext = encrypt(plaintext, key, iv)
print(f"Ciphertext: {ciphertext}")
decrypted_plaintext = decrypt(ciphertext, key, iv)
print(f"Decrypted plaintext: {decrypted_plaintext}")
דוגמה של הקוד הזה נמצאה בקוד שבדקתי לאחרונה (לא בסייברארק). זה קוד פייתוני שעובד ונלקח מדוגמת קוד אחרת או מ-LLM מלפני כשנה (לא הצלחתי לברר בדיוק). האם אתם יכולים לראות את הבעיה פה?
הבעיה היא לא אופרטיבית. הקוד הזה עובד ועובד היטב במחרוזות טקסט מסוימות. אבל יש איתו כמה בעיות עיקריות:
מפתח קצר מדי
המפתח הוא מרכיב מרכזי בתהליך ההצפנה והפענוח. תפקידו הוא להפוך את המידע הגלוי (ידוע גם כ-Plaintext) למידע מוצפן (הכינוי שלו הוא Ciphertext), ולהיפך, בהתאם לאלגוריתם הצפנה שנבחר. בתהליך הצפנה סימטרית, כמו AES (Advanced Encryption Standard), אותו מפתח משמש הן להצפנה והן לפענוח.
האורך של המפתח קובע את רמת האבטחה של ההצפנה. ככל שהמפתח ארוך יותר, כך קשה יותר לתוקף לפצח את ההצפנה על ידי ניסוי כל המפתחות האפשריים. ב-AES יש שלושה אורכים אפשריים למפתחות: 128 ביטים, 192 ביטים ו-256 ביטים.
במקרה הזה, המפתח היה באורך של 128 ביטים. שזה קצר ממה שאנחנו צריכים ברמה של ההצפנה. אין בעיה לעשות את זה באופן מודע, אבל פה זה ממש לא היה מודע.
IV
IV, או Initialization Vector הוא רצף של בתים באורך שנקבע על פי גודל הבלוק של האלגוריתם, ובמקרה של AES, אורך הבלוק הוא 128 ביטים. ה-IV משמש כנקודת התחלה בתהליך ההצפנה במצב CBC (Cipher Block Chaining). והמטרה שלו היא להבטיח שאותו טקסט בפועל שיוצפן פעמיים עם אותו מפתח יניב תוצאות שונות מבחינת מי שמסתכללמה זה חשוב? כי זה מגן מפני התקפות של ניתוח דפוסים, שמאד פופולרי עכשיו גם בעידן ה-LLM שמסוגל לזהות ניתוח דפוסים יותר בקלות. אם יש לנו IV סטטי, לא עשינו שום דבר.
ללא Padding
כיוון שאין padding, הטקסט המוצפן חייב להיות באורך שהוא כפולה של 16 בייטים, אחרת האלגוריתם לא יפעל. וזו גם היתה הבעיה שבגללה בכלל שמתי לב לבעיה הזו. כי התשתית שנכתבה עבדה מצויין עם תרחיש של טוקנים שהאורך שלהם היה הכפולה הזו, אבל עשה צרות צרורות כאשר ניסו להשתמש בו בדברים אחרים.
Padding נראה ככה:
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size)) # הוספת padding
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size) # הסרת padding
זו רק דוגמה אחת, אבל היא חוזרת על עצמה המון פעמים. שימוש נכון גם ב-IV וגם ב-Key הם קריטיים בכל הנוגע לקריפטוגרפיה נכונה שגם תואמת לתקנים והשגיאות האלו קורות שוב ושוב ושוב אצל אנשים שמממשים קריפטוגרפיה.
האם המסקנה של המאמר הזה היא לשים לב לדברים האלו כאשר מממשים קריפטוגרפיה לבד? התשובה היא לא. המטרה היא לשכנע אתכם שהסיפור הזה מורכב. במקרים (יש כאלו שיגידו: נדירים, יש כאלו שיגידו: לא) שאתם מממשים קריפטוגרפיה בעצמכם – חייבים להתייעץ עם קריפטוגרף – כיוון שיש עוד המון pitfalls שבתור מפתחים אנחנו יכולים ליפול בהם בקלות.
תגובה אחת
בשביל זה יש את NaCL