הצפנה: MISC::encode ו-CRYPT

מה עושים MISC::encode ו-decode לעומת CRYPT, באיזה מפתח הם משתמשים, מה חובה להגדיר בכל אתר לפני שיש נתונים, ומה קורה כשמסובבים מפתח.

⏱ 6 דק' קריאה 1035 מילים ערוך דף זה ב-GitHub

הליבה מציעה שני זוגות פונקציות הצפנה: MISC::encode / MISC::decode, שעליהם נשענים כמעט כל הטוקנים והעוגיות של המערכת, ו-CRYPT::encode / CRYPT::decode, ספרייה נקייה יותר שהליבה עצמה לא משתמשת בה. בעמוד הזה מתוארים שניהם, ההבדלים ביניהם, ומה אתם חייבים להגדיר כדי שהמפתחות לא יהיו ברירת המחדל הציבורית.

system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url.

מפתחות ברירת המחדל ידועים לכולם
MISC::encode מגיע עם מפתח ו-IV קבועים בקוד ('This is my secret key' ו-'This is my secret iv'), ו-CRYPT עם מפתח ברירת מחדל 'wizzocms'. הליבה לא מגדירה מפתחות משלה: אתר שלא קורא ל-MISC::set_encrypt_secret_keys() ב-application/includes/init.php רץ עם מפתחות שכל מי שקרא את הקוד מכיר. מי שמכיר את המפתח יכול לפענח וגם לזייף כל טוקן, עוגייה ושדה מוצפן שהאתר מנפיק. זה ממצאים SEC-02 ו-CORE-01 בביקורת, והסעיף הראשון בצ'קליסט ההתקשחות.

MISC::encode ו-MISC::decode#

public static function encode($str)
public static function decode($str)

encode מקבל כל ערך ש-PHP יכול לבצע לו serialize (מחרוזת, מספר, מערך) ומחזיר מחרוזת בטוחה לשימוש ב-URL ובעוגייה. decode מבצע את הפעולה ההפוכה ומחזיר את הערך המקורי.

פונקציהפרמטרמחזירה
MISC::encode($str)כל ערך שניתן לסריאליזציהמחרוזת מקודדת; מחרוזת ריקה מחזירה ""
MISC::decode($str)מחרוזת שנוצרה ב-encodeהערך המקורי, או false בפענוח כושל
$token = MISC::encode(["uid" => 17, "exp" => time() + 3600]);
// ... מעבירים ב-URL, בעוגייה או בשדה hidden ...
$data = MISC::decode($token);
if (is_array($data) && $data["exp"] > time())
{
    // תקף
}

איך זה עובד בפועל#

  1. הערך עובר serialize.
  2. MISC::encrypt_decrypt('encrypt', ...) מצפין ב-AES-256-CBC. המפתח הוא hash('sha256', $encrypt_secret_key), וה-IV הוא 16 התווים הראשונים של hash('sha256', $encrypt_secret_iv). כלומר ה-IV קבוע, לא אקראי.
  3. התוצאה עוברת base64_encode (הפונקציה הפנימית כבר מחזירה base64, כך שהתוצאה מקודדת פעמיים), ואז strtr שמחליף +/= ב--_, כדי שהמחרוזת תהיה בטוחה ל-URL.

המשמעות המעשית של העיצוב הזה:

  • אין חתימה (MAC). מי שמכיר את המפתח מייצר טוקן חוקי, ומי שלא מכיר אותו אינו מוגן מפני שינוי בתוכן המוצפן, אלא רק מפני שהפענוח ייתן זבל.
  • IV קבוע אומר שאותו ערך תמיד מקודד לאותה מחרוזת. אפשר לזהות ערכים זהים, וזה לא מתאים להצפנת מידע רגיש.
  • decode משתמש ב-unserialize בלי הגבלת מחלקות. כל עוד המפתח סודי זה לא ניתן לניצול מבחוץ; עם מפתח ברירת מחדל, מי שמזייף טוקן יכול להזריק אובייקטים. זה הטיעון החזק ביותר להחליף מפתחות (ממצאים SEC-03 ו-SEC-04).

מי משתמש ב-encode#

יש כ-80 קריאות ל-MISC::encode / decode בליבה. בין היתר:

  • עוגיית ההתחברות של משתמשי האתר (LOGIN, COOKIES::user_log), ראו משתמשי אתר.
  • שדות מוצפנים בטפסי הניהול, טוקנים של הגלריה ושל כלי החיתוך, וקישורים במיילים.
  • הרשימה המותרת של כתובות IP בפאנל הניהול (allow_ip).
  • שמות הקבצים של תמונות מטושטשות (STORAGE::get_blur_image).
  • עוגיית admin_token הישנה של פאנל הניהול (נתיב תאימות לאחור בלבד).

קביעת מפתחות לאתר#

public static function set_encrypt_secret_keys($key, $iv)
פרמטרמשמעות
$keyהסוד שממנו נגזר מפתח ההצפנה
$ivהסוד שממנו נגזר ה-IV

הליבה לא קוראת לפונקציה הזו בשום מקום, ולכן היא אחריות האתר. המקום הנכון הוא application/includes/init.php, שנטען בכל בקשה (כולל CLI ו-cron) אחרי CONFIG::init() ולפני כל קוד שמפענח משהו:

<?php
// application/includes/init.php
// הערכים מגיעים מקובץ מחוץ ל-docroot או מהכספת, לא מוטמעים בקוד שנכנס ל-git.
$keys = require CONFIG::$base_path . "/../secrets/wizzo_keys.php";

MISC::set_encrypt_secret_keys($keys["key"], $keys["iv"]);
CRYPT::set_secret_key($keys["crypt"]);

כל אחד מהערכים צריך להיות מחרוזת אקראית ארוכה, ייחודית לאתר:

openssl rand -hex 32
מגדירים בהתקנה, לא באמצע חיי האתר

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

CRYPT#

CRYPT (system/collections/CRYPT.php) היא מחלקה סטטית קטנה שנכתבה נכון יותר: IV אקראי בכל הצפנה, ו-JSON במקום serialize.

CRYPT::set_secret_key($key)   // ברירת מחדל: 'wizzocms'
CRYPT::encode($data)
CRYPT::decode($data)
פונקציההתנהגות
set_secret_key($key)קובעת את המפתח. הוא עובר hash('sha256', $key, true)
encode($data)מצפינה ב-AES-256-CBC עם IV אקראי של 16 בתים שמוצמד לתחילת התוצאה. המטען הוא JSON (JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK). הפלט הוא base64 בטוח ל-URL, ללא padding. ערך ריק או null מחזיר ""
decode($data)מפענחת ומחזירה את ערך ה-JSON; כשהתוכן המפוענח אינו JSON מוחזרת המחרוזת הגולמית
$blob = CRYPT::encode(["order" => 4521, "email" => "a@example.co.il"]);
$back = CRYPT::decode($blob);   // ["order" => 4521, "email" => "a@example.co.il"]

גם CRYPT לא מחתימה את המטען. מי שצריך לוודא שהתוכן לא שונה (למשל קישור "אשר הזמנה") מוסיף חתימת HMAC משלו:

$payload = CRYPT::encode(["order" => 4521]);
$sig     = hash_hmac("sha256", $payload, $keys["hmac"]);
$link    = "/confirm?d=" . $payload . "&s=" . $sig;

// בצד המקבל
$ok = hash_equals(hash_hmac("sha256", $_GET["d"], $keys["hmac"]), $_GET["s"]);

MISC או CRYPT#

MISC::encodeCRYPT::encode
IVקבוע (נגזר מהמפתח)אקראי לכל הצפנה
פורמט המטעןserializeJSON
פענוחunserialize ללא הגבלהjson_decode
ברירת מחדל של מפתח'This is my secret key''wizzocms'
שימוש בליבהרחב, אי אפשר להימנעאין
מתאים לקוד חדשלא, אלא כשחייבים תאימות לליבהכן

לקוד חדש של האתר בחרו CRYPT. השתמשו ב-MISC::encode רק כשאתם צריכים להבין או לייצר ערכים שהליבה עצמה קוראת, למשל עוגיית התחברות.

הספרייה encoder
system/libraries/encoder.php (המחלקה encoder) היא גרסה ישנה: AES-128-CBC עם מפתח קבוע בקוד ו-gzcompress(serialize()). הליבה לא משתמשת בה ואין סיבה להתחיל להשתמש בה.

סיסמאות הן לא עניין של הפונקציות האלה#

אסור להצפין סיסמאות עם encode או CRYPT: סיסמאות עוברות hash חד-כיווני. הליבה כוללת את המחלקה Bcrypt (system/libraries/bcrypt.php) עם hash($input) ו-verify($input, $hash), והפונקציה הגלובלית dt_encode($password) מחזירה hash של bcrypt. פרטים בעמוד משתמשי אתר.

ראו גם#

מצאתם טעות או חוסר? תקנו את הדף או פתחו Issue בריפו. התיעוד נכתב מתוך הקוד של ליבה 5.0.115.