מהי בעיית Key Reuse בעבודה עם מצפין זרם

22/05/2019

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

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

מימוש מצפין זרם פופולרי (ולא הכי מאובטח) נקרא RC4 והקוד שלו בפייתון הוא בסך הכל:

def rc4(data, key):
    """RC4 encryption and decryption method."""
    S, j, out = list(range(256)), 0, []

    for i in range(256):
        j = (j + S[i] + ord(key[i % len(key)])) % 256
        S[i], S[j] = S[j], S[i]

    i = j = 0
    for ch in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out.append(chr(ord(ch) ^ S[(S[i] + S[j]) % 256]))

    return "".join(out)

הטעות הכי גדולה של אנשים שמשתמשים במצפין זרם נקראת Key Reuse. זה אומר להשתמש באותו Key Stream כדי להצפין מספר הודעות. ולמה זה רע? כי בגלל הטבע של XOR במצב כזה המפתחות "מתבטלים". במתמטיקה זה אומר ש:

C1 = Message1 ^ Key
C2 = Message2 ^ Key
=>

C1 ^ C2 = M1 ^ Key ^ M2 ^ Key
C1 ^ C2 = M1 ^ M2

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

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

from rc4 import rc4

enc1 = rc4('hello world', 'secret')
enc2 = rc4('\x00' * 10, 'secret')

with open('msg1.bin', 'w') as f:
    f.write(enc1)

with open('msg2.bin', 'w') as f:
    f.write(enc2)

with open('msg_xor.bin', 'w') as f:
    xor_enc = ''.join([chr(ord(a) ^ ord(b)) for a,b in zip(enc1,enc2)])
    f.write(xor_enc)

נמשיך לראות את הקבצים שנוצרו:

$ xxd msg1.bin
00000000: c285 53c2 be70 c3ad c284 c2a1 c389 40c2  ..S..p........@.
00000010: a7c3 9f                                  ...

$ xxd msg2.bin 
00000000: c3ad 36c3 921c c282 c2a4 c396 c2a6 32c3  ..6...........2.
00000010: 8b                                       .

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

$ xxd msg_xor.bin 
00000000: 6865 6c6c 6f20 776f 726c                 hello worl

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

תתפלאו לשמוע שהפיתרון במצבים כאלה הוא מאוד קל - פשוט צריך להוסיף ערך ייחודי למפתח כדי שה Key Stream יהיה שונה לכל הודעה שנצפין. הנה התוכנית המתוקנת שלנו:

from rc4 import rc4

enc1 = rc4('hello world', 'secret1')
enc2 = rc4('\x00' * 10, 'secret2')

with open('msg1.bin', 'w') as f:
    f.write(enc1)

with open('msg2.bin', 'w') as f:
    f.write(enc2)

with open('msg_xor.bin', 'w') as f:
    xor_enc = ''.join([chr(ord(a) ^ ord(b)) for a,b in zip(enc1,enc2)])
    f.write(xor_enc)

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

$ xxd msg_xor.bin 
00000000: c398 6ac2 9070 0b0e 3257 c3bb 5d         ..j..p..2W..]