מהנושא במגזין ליישום בפרויקט
דפי שירות וטכניים רלוונטיים למאמר
בהצפנת AES Delphi הבעיה בפועל נדירה שתהיה ב־“AES עצמו“, והרבה יותר קשורה לתנאי שולי מערכת: יש לעבד נתונים כזרם (קבצים, BLOBs, גיבויים), פורמטים ישנים חייבים להישאר ניתנים לקריאה, ובסביבת הפעלה נדרשת יכולת דיבוג (Header, גרסאות) והגדרות ברירת מחדל בטוחות (Salt/IV אקראיים, ללא שימוש חוזר). קטע הקוד הזה מראה לכן לא רק „Encrypt/Decrypt“, אלא פורמט קטן ועמיד עם Header, גרסה, Salt ו‑IV – בנוסף PBKDF2 להסקת המפתח ונקודה שבה ניתן להשלים אימות שלמות באופן מושכל.
מדוע „AES-String verschlüsseln“ כמעט אף פעם אינה מספיקה
בהתוכנה ארגונית מותאמת מופיעה הצפנה בדרך כלל בשלוש נקודות: (1) תצורה/סודות (למשל פרטי גישה), (2) קבצי חילוף/ייצוא ו‑(3) נתונים ברי שימור (למשל ארכיבים, מכולות מסמכים). הגישה התמימה „סיסמה → מפתח AES → מחרוזת פנימה/חוצה“ מתערערת במהירות:
- שימוש חוזר ב‑IV: במודים כמו CBC או GCM חייב וקטור אתחול (IV) להיות ייחודי לכל הצפנה. IV קבוע הוא דליפה, גם אם הסיסמה חזקה.
- מפתח מתוך סיסמה ללא KDF: שימוש בסיסמה ישירות כמפתח (או חישוב hash חד־פעמי) מאפשר התקפות לא מקוונות. KDF (Key Derivation Function) כמו PBKDF2 מאט באופן מכוון את התוקף.
- אין גרסת פורמט: בלי Header/גרסה קשה לשנות מאוחר יותר פרמטרים כמו מספר איטרציות, אלגוריתם או פרמטרים אחרים מבלי «להשאיב» או לאבד נתונים ישנים.
- אין אימות שלמות: AES‑CBC מצפין אך אינו מונע מניפולציה. בלי אימות (למשל HMAC או AEAD כמו GCM) תקבלו בעיות Bitflipping/_PADDING_ ותמונת שגיאות שקשה לאבחן.
ליבת המאמר הזה: פורמט מכולה קטן שתומך בזרימה (streaming), ניתן לגרסה ומונע את שגיאות הברירת מחדל הנפוצות.
AES Verschlüsselung Delphi mit Header, Salt, IV und PBKDF2
אנחנו מגדירים פורמט מכולה פשוט שניתן להשתמש בו גם ב‑BLOBs של מסד נתונים או ב‑Message‑Payloads:
- Magic: 4 בתים, למשל
NBAE(בדיקת „האם זה הפורמט שלנו?“ מהירה) - Version: 1 בת (מאפשר מיגרציה)
- KDF‑Parameter: מספר איטרציות (4 בתים)
- Salt: 16 בתים (אקראי לכל קובץ)
- IV: 16 בתים (אקראי לכל קובץ עבור AES‑CBC)
- Ciphertext: נתוני המשתמש המוצפנים (מתאימים לזרימה)
חשוב: Salt ו‑IV אינם סודיים. הם רק חייבים להיות חדשים לכל פעולה של הצפנה. הסיסמה נשארת סודית; המפתח המופק ממנה אינו נשמר.
AES Verschlüsselung Delphi im Stream: Container schreiben/lesen
הקוד נכתב במודע כ“תוכנית בנייה“: פונקציות מופרדות בבירור, Header שניתן לבדוק, אין Hidden‑Globals. עבור AES ו‑PBKDF2 צוותים רבים עושים שימוש בספריית קריפטו מבוססת (למשל DEC). הקטע ממחיש את הפורמט ואת תבנית הזרימה; קריאות ה‑AES/‑PBKDF2 מנוקדות באופן שניתן להחליפן בהתאם לספרייה שבחרתם.
unit Nb.AesContainer;
interface
uses
System.SysUtils, System.Classes, System.NetEncoding;
type
ENbCryptoError = class(Exception);
TNbAesContainer = class
public
class procedure EncryptStreamToStream(const AIn: TStream; const AOut: TStream;
const APassword: string; const AIterations: Cardinal = 200000);
class procedure DecryptStreamToStream(const AIn: TStream; const AOut: TStream;
const APassword: string);
class function EncryptBytesToBase64(const APlain: TBytes; const APassword: string): string;
class function DecryptBase64ToBytes(const ACipherB64: string; const APassword: string): TBytes;
end;
implementation
const
CMagic: array[0..3] of AnsiChar = (‚N‘,’B‘,’A‘,’E‘);
CVersion: Byte = 1;
CSaltLen = 16;
CIvLen = 16;
type
TNbHeaderV1 = packed record
Magic: array[0..3] of AnsiChar;
Version: Byte;
Iterations: Cardinal; // little endian
Salt: array[0..CSaltLen-1] of Byte;
IV: array[0..CIvLen-1] of Byte;
end;
// — תלותיות, שעליכם לממש בהתאם ל־Crypto-Stack —
procedure FillRandomBytes(var B: TBytes);
begin
// עבור אקראיות קריפטוגרפית: להשתמש ב־OS-CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). כאן משמש כפלייסהולדר בכוונה.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG לא מחובר‘);
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// מימוש, למשל עם DEC (PBKDF2) או ספרייה אחרת.
// תוצאה: AKeyLen בתים.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: לא מחובר‘);
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// מימוש באמצעות ספרייה:
// – KeyLen = 32 בתים
// – IVLen = 16 בתים
// – PKCS#7 Padding
// חשוב: לעבד בגישה מבוססת זרם (stream-oriented), לא להעמיס הכל בזיכרון.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: לא מחובר‘);
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: לא מחובר‘);
end;
// — עזר —
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header לא נכתב‘);
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Header לא שלם‘);
if (H.Magic[0] <> CMagic[0]) or (H.Magic[1] <> CMagic[1]) or
(H.Magic[2] <> CMagic[2]) or (H.Magic[3] <> CMagic[3]) then
raise ENbCryptoError.Create(‚אין Container תקין (ה־Magic אינו תואם)‘);
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚גרסת Container לא ידועה: %d‘, [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚מספר האיטרציות מחוץ לטווח סביר‘);
Result := H;
end;
class procedure TNbAesContainer.EncryptStreamToStream(const AIn, AOut: TStream;
const APassword: string; const AIterations: Cardinal);
var
H: TNbHeaderV1;
Salt, IV, Key: TBytes;
begin
if APassword = “ then
raise ENbCryptoError.Create(‚הסיסמה לא יכולה להיות ריקה‘);
// יצירת Salt/IV
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// מילוי Header
Move(CMagic[0], H.Magic[0], Length(CMagic));
H.Version := CVersion;
H.Iterations := AIterations;
Move(Salt[0], H.Salt[0], CSaltLen);
Move(IV[0], H.IV[0], CIvLen);
WriteHeaderV1(AOut, H);
// הפקת המפתח (32 בתים עבור AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// הצפנת נתוני התוכן (Ciphertext יבוא מיד אחרי ה־Header)
AES256_CBC_EncryptStream(Key, IV, AIn, AOut);
end;
class procedure TNbAesContainer.DecryptStreamToStream(const AIn, AOut: TStream;
const APassword: string);
var
H: TNbHeaderV1;
Salt, IV, Key: TBytes;
begin
if APassword = “ then
raise ENbCryptoError.Create(‚הסיסמה לא יכולה להיות ריקה‘);
H := ReadHeaderV1(AIn);
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
Move(H.Salt[0], Salt[0], CSaltLen);
Move(H.IV[0], IV[0], CIvLen);
Key := PBKDF2_HMAC_SHA256(APassword, Salt, H.Iterations, 32);
// פענוח מהמצב הנוכחי של הזרם (אחרי Header)
AES256_CBC_DecryptStream(Key, IV, AIn, AOut);
end;
class function TNbAesContainer.EncryptBytesToBase64(const APlain: TBytes;
const APassword: string): string;
var
InS, OutS: TBytesStream;
begin
InS := TBytesStream.Create(APlain);
try
OutS := TBytesStream.Create;
try
EncryptStreamToStream(InS, OutS, APassword);
Result := TNetEncoding.Base64.EncodeBytesToString(OutS.Bytes, 0, OutS.Size);
finally
OutS.Free;
end;
finally
InS.Free;
end;
end;
class function TNbAesContainer.DecryptBase64ToBytes(const ACipherB64,
APassword: string): TBytes;
var
Cipher: TBytes;
InS, OutS: TBytesStream;
begin
Cipher := TNetEncoding.Base64.DecodeStringToBytes(ACipherB64);
InS := TBytesStream.Create(Cipher);
try
OutS := TBytesStream.Create;
try
DecryptStreamToStream(InS, OutS, APassword);
Result := OutS.Bytes;
SetLength(Result, OutS.Size);
finally
OutS.Free;
end;
finally
InS.Free;
end;
end;
end.
מטרה: מכולה מינימלית המתאימה לקבצים ול־BLOBs, כולל גרסאות ופרמטרי KDF. הגבלות: יש לספק חיבור ממשי ל־CSPRNG של המערכת (אקראיות קריפטוגרפית מאובטחת מה־OS) ולממש מתחתיה יישום AES/PBKDF2 יציב. סכנות נפוצות: אל תשתמשו ב“איזה Random“ (לא ב־Random()), אין להשתמש ב‑IVs קבועים, ותכננו טיפול שגיאות ברור בזמן דה־קריפט (סיסמה שגויה לעומת נתונים פגומים). וריאציות: במקום CBC עדיף AEAD (ראו למטה), או להרחיב את הכותרת עם מזהה אלגוריתם ו‑HMAC.
שלמות: מדוע AES‑CBC בפני עצמו מסוכן בזמן ריצה
AES‑CBC עדיין קיים בהרבה הקשרים ישנים ויכול לעבוד אם אתם מוסיפים בנוסף מנגנון אימות שלמות. ללא אימות שלמות תוקף יכול למניפול את הציפֶר‑טֶקסט; גם ללא תוקף פעיל שגיאות בהעברה או שכבות אחסון פגומות יפיקו שגיאות „Padding“ שקשה לאבחן.
אפשרויות פרגמטיות:
- Encrypt‑then‑HMAC: כתבו לאחר הציפרטקסט HMAC (למשל HMAC‑SHA‑256) על Header+Ciphertext. בקריאה — בדקו קודם את ה‑HMAC ואז פענחו. לשם כך נגזרו עדיף שני מפתחות מ‑PBKDF2 (למשל 64 בתים: 32 ל‑AES, 32 ל‑HMAC), במקום להשתמש באותו מפתח לשני התפקידים.
- AES‑GCM: מצב AEAD (Authenticated Encryption with Associated Data). נותן ציפרטקסט + תג אימות. זו לעתים הבחירה הנקייה כיום, אם הספרייה Delphi שלכם תומכת ב‑GCM בצורה יציבה. שדות בכותרת יכולים להיות מאומתים כ־“AAD“ בלי להצפין אותם.
אם אתם חייבים להישאר עם CBC (למשל לצורך תאימות), Encrypt‑then‑HMAC היא ההשלמה החזקה. עבור פורמטים חדשים כדאי לשקול GCM, כי הוא מספק אימות ונותן דפוסי שגיאה ברורים יותר.
חשוב במיוחד: „אקראיות קריפטוגרפית“ ולמה System.Hash לא מספיק
רפלקס ישן נפוץ בפרויקטים של Delphi: „אנחנו פשוט עושים SHA256 על חותמת זמן + משהו וקיבלנו Random.“ זו אינה יסוד מהימן. עבור salt ו‑IV אתם זקוקים ל‑CSPRNG (Cryptographically Secure Pseudo Random Number Generator) של מערכת ההפעלה. תחת Windows זה בדרך כלל BCrypt‑API (CNG), ותחת Linux זה גנרטור קרנל כמו getrandom() או /dev/urandom. ההבדל מעשי: CSPRNG מיועד כך שמערך תצפיות לא יאפשר חיזוי של ערכים עתידיים.
תחבורת ארכיטקטונית: עטפו את זה ביחידת „RandomProvider“ קטנה שניתן ל־mock בבדיקות. כך תפתרו שני מקרים קיצוניים בו זמנית: בדיקות בר־שחזור (עם Seed קבוע ב‑Mock) ובטיחות אמיתית בסביבת ייצור (עם OS‑CSPRNG). זה מונע מצב שבו ב־hotfix מחדירים בחזרה את Random() כי זה מהיר.
ניפוי שגיאות ומיגרציה של מערכות ישנות: ניהול גרסאות זה לא מותרות
הכותרת אינה רק ל“יופי קריפטוגרפי“, אלא לתחזוקה:
- כוונון איטרציות: מספר איטרציות ב‑PBKDF2 משתנה עם השנים. עם שדה כותרת תוכלו להגביר את הערך מאוחר יותר מבלי להפוך נתונים ישנים לבלתי קריאים.
- שינוי פורמט: גרסה 2 יכולה לעבור ל‑AES‑GCM או להוסיף HMAC.
- אבחון בשטח: Magic/Version מאפשרים בדיקות מהירות בלוגים ובכלים בלי לפענח את הנתונים.
טיפ מעשי: יישמו אינספקטור קטן שמקרא רק את ה-Header (Magic/Version/Iterations) וכותב אותו ללוג. כך תוכלו להבהיר הרבה מקרים של תמיכה („איזו גרסה יש כאן?“) ללא טיפול בסיסמאות.
הגירה נקייה: „Read old, write new“ במקום Big Bang
אם אתם מחליפים פורמט ישן (למשל IV קבוע, חוסר KDF, Blowfish/3DES, או XOR שנבנה בעצמכם), בפרויקטים של Delphi הוכחה תבנית עבודה: בעת קריאה מזהים מספר פורמטים (Magic/Version או היוריסטיקת Fallback), ובעת כתיבה מייצרים רק את הפורמט החדש. בנוסף, ניתן לאחר פענוח מוצלח לבצע ברקע re-encrypt („lazy migration“), אם זה מתאים לתהליך. כך אתם מצמצמים את סיכון הרולאאוט ומנעים את הצורך ב“לצפין הכל מחדש“ כחלון תחזוקה.
Threading und Streaming: typische Kanten in Delphi
הצפנה מתבצעת לעיתים קרובות ב-Worker-Threads (למשל ביצוא, בהעלאה ל-פורטל לקוח, בכתיבת ארכיונים גדולים). שתי נקודות שמופיעות באופן קבוע בפרויקטים של Delphi:
- מיקום ה-Stream: לפני הצפנה/פענוח דרשו חוזים ברורים: ה-Input-Stream ייקרא מה-Position הנוכחי, וה-Output-Stream יישמר מה-Position הנוכחי. כאשר משתמשים שוב ב-Streams יש להגדיר במודע
Position := 0. - שיאי זיכרון: הימנעו מ“הכל ב-TBytes“. גישת ה-Stream חשובה במיוחד לקבצים גדולים. אם ספריית הקריפטו שלכם מקבלת רק מערכי בתים, כדאי להשקיע בעבודה נוספת ולעבור למימוש תומך-Stream או לבנות מתאם עם buffering.
אם אתם מצפינים בתוך Services (Windows- או Linux-Services), שימו לב גם ל-Exception-Logging מסודר: „סיסמה שגויה“, „Header פגום“, „Tag/HMAC לא תקין“ הן מקרים תפעוליים נפרדים ויש להבחין ביניהם. חשוב: הודעות שגיאה החוצה אינן צריכות להיות מפורטות מדי (אין לציין „Padding שגוי בבלוק 7“ כשגיאת API), אך בלוג הפנימי יש לפרט מספיק כדי לאבחן את המקרה.
מתי הגישה משתלמת — והיכן היא עלולה לקרוס
משתלמת אם אתם: (a) מאחסנים לטווח ארוך נתוני יצוא/יבוא מוצפנים, (b) מפעילים גרסאות תוכנה שונות במקביל, (c) מעבדים נתונים כ-Streams, או (d) זקוקים לממשק קריפטוגרפי נקי למספר מודולים (Client/Server/Tooling).
עלולה לקרוס אם אתם מנסים לפתור איתה „הכל“: ל-Transport אחראי TLS, לא עטיפה AES ביתית. ל-Secrets (סיסמאות, Tokens) לרוב מתאים יותר OS-Secret-Store או Vault. ואם אתם צריכים אינטראופרביליות עם שפות אחרות, יש לתעד בדיוק את ה-Header, ה-Endianness וה-Encoding (או להשתמש בפורמט מקובל).
מסקנה: AES ב-Delphi הוא פחות אלגוריתם, יותר הנדסה
התועלת האמיתית של הקטע הזה אינה „AES רץ“, אלא פורמט שניתן לתפעול: Salt ו-IV אקראיים, Header מגרסה, פרמטרי PBKDF2 ב-Payload ועיבוד תומך-Stream. השלימו בפורמטים חדשים ככל האפשר את בדיקות השלמות (Integrity) — למשל AES-GCM או Encrypt-then-HMAC. כך מה שהיה „אנחנו מצפינים משהו“ הופך לרכיב שניתן לתחזק ולהגר לשדרוגים במערכות ארגוניות דיגיטליות גם אחרי שנים.
אם עליכם לשלב מיכל כזה בסביבה Delphi מבוססת שהתפתחה לאורך זמן או להעבירו באופן מסודר מפורמט Legacy, מומלץ לערוך בדיקת ארכיטקטורה קצרה (ניהול מפתחות, גרסאות פורמט, תפעול/לוגים). פרטים נבהיר לפי הצורך בשיחה:
בהקשר המקצועי גם Delphi AES ו-PBKDF2 Delphi ממלאים תפקיד חשוב, כאשר אינטגרציות, זרימות נתונים והמשך פיתוח חייבים להתממשק באופן נקי.
השלב הבא
כאשר הנושא הופך לפרויקט ממשי, יש לבחון כבר בשלב מוקדם את הארכיטקטורה, המצב הקיים והתפעול יחד.
אנו תומכים לא רק בשאלות נקודתיות, אלא גם כשמקטעי קוד מקור, נושאי Legacy או רעיונות פורטל אמורים להפוך לפרויקט ארגוני מהימן ועמיד.
- המצב הקיים, תמונת היעד והסיכונים הטכניים מוערכים יחד.
- REST, גישה לנתונים, פורטלים ו-Rollout לא יידחו כתוצאות מאוחרות.
- אתם מזהים מוקדם איזה נתיב בר-קיימא מבחינה כלכלית ותפעולית.