סקלרים ומחרוזות

crypt#

hashing חד־כיווני בסגנון passwd(5) של מחרוזת טקסט גלוי עם salt.

crypt הוא עטיפה דקה מעל פונקציית crypt(3) של ספריית C של המערכת. הוא הופך את PLAINTEXT ו־SALT למחרוזת קצרה - digest - ומחזיר אותה. אותם PLAINTEXT ו־SALT תמיד מפיקים אותו digest; שינויים קטנים בכל אחד מהם מפיקים digest שונה לחלוטין. אין פונקציה הפוכה: לא ניתן לשחזר את PLAINTEXT מ־digest.

אזהרה

crypt אינו פונקציית הצפנה לשימוש כללי, למרות שמו. לא ניתן לבטל אותו. הוא גם אינו hash סיסמאות מודרני ברוב הקונפיגורציות המסורתיות - הסכמה ההיסטורית של Unix מתחשבת רק בשמונת הבתים הראשונים של PLAINTEXT ומפיקה digest של 13 בתים מעל salt של 12 ביט, מה שאינו מתאים להגנה על סיסמאות מפני תוקפים בני זמננו. מה ש־crypt טוב בו הוא אימות מחרוזת שסופקה על ידי המשתמש מול digest מאוחסן שהופק על ידי crypt של אותה מערכת. עבור כל דבר אחר - הצפנה, גזירת מפתח, או hashing של נתונים גדולים - השתמשו בספריית קריפטוגרפיה אמיתית. ראו ראו גם להלן.

תקציר#

crypt PLAINTEXT, SALT

שני הארגומנטים הם מחרוזות. ערך ההחזרה הוא digest של מחרוזת.

מה מקבלים בחזרה#

מחרוזת שצורתה המדויקת תלויה במימוש crypt(3) של המארח. מסורתית מדובר ב־13 בתים: שני בתים של salt ואחריהם 11 בתים מתוך הקבוצה [./0-9A-Za-z]. glibc, musl, ו־BSD libcs מודרניים מקבלים saltים מורחבים הבוחרים סכמות חזקות יותר (MD5, SHA-256, SHA-512, bcrypt, yescrypt) ומחזירים digestים ארוכים יותר בפורמט קובץ ה־passwd $id$salt$hash.

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

if (crypt($plain, $digest) eq $digest) { ... }

ה־salt ששימש ליצירת ה־digest מוטמע ב־digest, כך שזה מריץ מחדש את אותה סכמה עם אותם פרמטרים על הטקסט הגלוי המועמד. קוד שנכתב כך עובד עם הסכמה הקלאסית של DES ועם כל הרחבה מודרנית ש־crypt(3) נתון חושף, מבלי שצריך לדעת אילו מהן הפיקה את ה־digest המאוחסן.

מצב גלובלי שהוא נוגע בו#

crypt אינו קורא או כותב משתנה מיוחד של Perl. על קלט Unicode הוא עשוי לפלוט שגיאה קטלנית Wide character in crypt (ראו מקרי קצה).

דוגמאות#

אימות סיסמה מול digest מאוחסן בסגנון passwd:

my $stored = (getpwuid($<))[1];       # encrypted field from passwd

print "Password: ";
chomp(my $word = <STDIN>);

if (crypt($word, $stored) eq $stored) {
    print "ok\n";
} else {
    die "Sorry...\n";
}

יצירת digest רענן עבור סיסמה חדשה באמצעות salt אקראי בן שני תווים מהאלפבית הקלאסי. הקבוצה היא רק המלצה; התווים המדויקים המתקבלים ב־salt הם כל מה ש־crypt(3) של המארח מאפשר, ו־Perl אינו מטיל הגבלה משלו:

my @chars = ('.', '/', 0..9, 'A'..'Z', 'a'..'z');
my $salt  = $chars[rand 64] . $chars[rand 64];
my $hash  = crypt($plaintext, $salt);

בקשת סכמה מודרנית ספציפית באמצעות שימוש ב־salt מורחב (glibc ו־libcs תואמים). הקידומת $id$ בוחרת את האלגוריתם - $1$ הוא MD5-crypt, $5$ הוא SHA-256-crypt, $6$ הוא SHA-512-crypt, $2b$ הוא bcrypt היכן שנתמך:

my $hash = crypt($plaintext, '$6$rounds=100000$' . $random_salt);

אימות חוזר באמצעות שימוש ב־digest המאוחסן כ־salt - חסר־תלות בסכמה:

my $candidate = crypt($plaintext, $stored_digest);
my $match     = $candidate eq $stored_digest;

השוואה בזמן קבוע של שני ה־digestים היא עדיין באחריות הקורא; eq רגיל מדליף מידע תזמון. אם מודל האיום אכפת לו, השוו בית־מול־בית בלולאה שאינה מקצרת, או השתמשו בשגרת השוואה ממודול קריפטו.

מקרי קצה#

  • קטימת שמונת הבתים הראשונים על DES קלאסי. ב־crypt(3) של Unix מסורתי, רק שמונת הבתים הראשונים של PLAINTEXT משפיעים על ה־digest: crypt("password123", $s) ו־crypt("password456", $s) מתנגשים. סכמות מורחבות ($1$, $5$, $6$, $2b$, …) אינן בעלות מגבלה זו, אך הסכמה הקלאסית היא מה שמקבלים מ־salt דו־בייטי ללא קידומת.

  • הנחות על אורך salt שגויות. אל תניחו ש־substr($digest,0,2) הוא ה־salt. digestים מורחבים נושאים את ה־salt במיקום ובאורך שונים. השתמשו שוב ב־digest המלא כארגומנט ה־salt (crypt($plain, $digest)) ותנו ל־crypt(3) לפרסר אותו.

  • קלט Unicode. אם PLAINTEXT או SALT מכילים תווים מעל נקודת קוד 255, Perl מנסה להוריד דרגה של עותק של המחרוזת לבתים לפני קריאה ל־crypt(3). אם כל תו נכנס בבית, הורדת הדרגה מצליחה בשקט. אם תו כלשהו לא, crypt מת עם Wide character in crypt. קודדו במפורש (לדוגמה Encode::encode_utf8) אם הטקסט הגלוי שלכם עשוי להכיל תווים רחבים.

  • סכמה שאינה נתמכת מחזירה כישלון. העברת קידומת $id$ ש־crypt(3) של המארח אינו מבין מחזירה בדרך כלל undef או מחרוזת קצרה המתחילה ב־*0 או *1. תמיד בדקו את צורת ההחזרה לפני השימוש בה.

  • crypt(3) עשויה להיעדר או להיות מוגבלת. על מספר קטן של מארחים פונקציית הספרייה הוסרה או הוחלפה ב־stub מסיבות של בקרת ייצוא. crypt ב־Perl מסוגלת רק כפי שקריאת ה־libc הבסיסית מסוגלת; אם קריאת ה־libc היא stub, כך גם זו.

  • אינו מתאים ל־hashing של נתונים גדולים. crypt מתעלם מרוב הקלט שלו בסכמה הקלאסית והוא איטי לכל קריאה בסכמות המתוחות. הוא הכלי השגוי ל־checksumming של קבצים או בניית אחסון מבוסס־תוכן.

הבדלים מהמקור#

תאימות מלאה עם Perl 5.42 המקורי. pperl מעביר ל־crypt(3) של המארח באותו אופן ש־perl5 עושה, כך שפורמטי ה־salt המתקבלים וצורת ה־digest המדויקת תואמים את כל מה שספריית המערכת מפיקה - כולל זמינות סכמות מורחבות $1$/$5$/$6$/ $2b$.

ראו גם#

  • Digest::MD5 - MD5-128 מעל מחרוזות בתים שרירותיות; השתמשו לטביעת אצבע של נתונים, לא לאחסון סיסמאות

  • Digest::SHA - digestים ממשפחת SHA-1/2; השתמשו לבדיקות שלמות וכאבן בניין ל־HMAC ולגזירת מפתח

  • Crypt::Bcrypt, Crypt::Argon2, Crypt::Passphrase - מודולי CPAN ל־hashing מודרני של סיסמאות; העדיפו אותם על פני crypt עבור כל קוד חדש לאימות סיסמאות

  • Crypt::Eksblowfish, Crypt::ScryptKDF - מודולי CPAN לגזירת מפתח מבוססת bcrypt ו־scrypt

  • getpwuid - שליפת שדה הסיסמה־המוצפנת ממסד נתוני ה־passwd המקומי; השדה השביעי שלו הוא ה־digest שמעבירים כ־SALT בעת אימות

  • chomp - הסירו את השורה החדשה הגוררת מסיסמה שנקראה עם <STDIN> לפני העברתה ל־crypt