EurekaLog offers 3 units:
EEncoding
- contains data encoding and transformation functions;EHash
- contains hash functions;EEncrypt
- contains symmetric and asymmetric encryption functions.
Important Note: please update EurekaLog to the most recent version. Not all features described here are available in previous versions, because some features were published specifically for this article.
Encoding
Before talking about cryptography, you need to take decicion about data representation. For example, suppose you want to get the MD5 hash of the'Привет'
string (means "Hello" in Russian, reads as "Privet", stress on the second syllable). What exactly are you gonna feed into hash function? $CF$F0$E8$E2$E5$F2 bytes? (which is 'Привет'
encoded via ANSI/Windows-1251) Or $1F$04$40$04$38$04$32$04$35$04$42$04 bytes? ('Привет'
in Unicode/UTF-16) Or may be $D0$9F$D1$80$D0$B8 bytes ('Привет'
in UTF-8)? Depending on how you answer this question, you will get different results. For example, the MD5 hash for the 'Привет'
in UTF-16 would be 8EFA2364EE560EE1B862ECC8D430C9AD, for 'Привет'
in ANSI - 43A3F987A7AF93811B7682E43ED0752A, and for 'Привет'
in UTF-8 - 8A669E9418750C81AB90AE159A8EC410.Questions like this probably don't matter if you use cryptography functions exclusively inside your own apps. But as soon as you need to interact with other code - you immediately would have problems with the exact definition of the data.
Therefore, when you want exact result, you should operate on bytes, not strings. In Delphi, to operate on bytes, you can:
- Use pointer + size of data:
(const ABuffer: Pointer; const ABufferSize: Cardinal)
; - Use
TBytes
(array of Byte
- dynamic byte array); - Use
RawByteString
; - Use
TStream
(its sub-classes).
Specifically, EurekaLog functions accepts pointer+size, as well as overloaded option for
RawByteString
.For example, if you try to obtain MD5-hash from "just" string
'Привет'
in PHP - you would get 8a669e9418750c81ab90ae159a8ec410 - i.e. MD5-hash of UTF-8 encoded 'Привет'
.From where you can also conclude that strings in PHP are stored in UTF-8; for comparison: Delphi stores strings as UTF-16 (since Delphi 2009) or ANSI (Delphi 2007 and earlier).If you want to change the encoding in PHP, you will need to call something like
mb_convert_encoding
. And if you want to change the encoding in Delphi, you need Delphi encoding functions. Specifically, to convert to/from UTF-8, TEncoding
. In Delphi 2009 and up, you can also just declare the string type of the desired encoding and string data conversion will be done automatically when assigned.The same is true in the opposite direction: the result of calling cryptographic functions is a set of bytes (hash, encrypted data, etc.). If you want to display these bytes to a human, you have to convert it to a string. It can be done, again, in different ways. For example, you can use the built-in function
BinToHex
or its more convenient equivalents: HexEncodeString
/HexEncodeToString
from EurekaLog. You can use Base64EncodeString
/Base64EncodeToString
from EurekaLog. If, suddenly, you need to convert data from/to RawByteString
, then EurekaLog has RAWToString
/RAWFromString
helpers. Also you may want to load/save small data directly to files - there is FileToString
/StringToFile
for that (from the ECompatibility
unit).Examples of using the mentioned functions can be found below.
Hashing
EurekaLog has functions for calculating the following hashes:- CRC16
- CRC32
- MD5
- SHA-1
- SHA-256
- SDBM is a good general purpose hash function with uniform distribution, convenient for use as a key/index in a database
MD5Hash()
), returns result of THashNameHash
type (for example, TSHA1Hash
), and accepts RawByteString
on input, as well as pointer+size (overloaded option).Additionally, EurekaLog has HMAC implementation for some hashes. One way to use HMAC is to authenticate a user by combining a salt and a password to obtain hash via HMAC. HMAC functions have names like HashNameHMAC (for example,
MD5HMAC()
) and accepts password and salt on input.Here are some practical examples:
1. Calculate hash of a string:
uses EEncoding, // for HexEncodeToString EHash; // for MD5Hash procedure TForm1.Button1Click(Sender: TObject); var S: String; // Source string UTF8Str: UTF8String; // Byte representation of a string Hash: TMD5Hash; // Result begin // Define source data S := 'Привет'; // Define exact representation as bytes // We use UTF-8 in this example UTF8Str := UTF8Encode(S); // (you can also just do UTF8Str := S; in Delphi 2009 and up) // Calculate hash from bytes Hash := MD5Hash(UTF8Str); // Show hash to a human Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash)); // Should display '8A669E9418750C81AB90AE159A8EC410' end;
2. Calculate hash of a file:
uses EEncoding, // for HexEncodeToString EHash, // for SHA256Hash ECompatibility; // for FileToString procedure TForm1.Button1Click(Sender: TObject); var Content: RawByteString; // File's bytes Hash: TSHA256Hash; // Result begin // Loads entire file into memory Content := FileToString(ParamStr(0)); // Content will be something like 'MZP'#0#2#0#0#0... // Calculate hash from bytes Hash := SHA256Hash(Content); Finalize(Content); // optional // Show hash to a human Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash)); // Should be something like 'FCF52FDC753E3797FE5EE4B5A7680E656D044D6BF7D97C408F0F7874492E43C2' end;
3. Calculate hash of a string in an arbitrary encoding:
uses EEncoding, // for HexEncodeToString (also for TEncoding for older Delphi) EHash; // for CRC32Hash procedure TForm21.Button1Click(Sender: TObject); var S: String; // Source string Encoding: TEncoding; // Encoding to encode the string Content: TBytes; // String's bytes Hash: TCRC32Hash; // Result begin // Define source data S := 'Привет'; // Define the encoding Encoding := TEncoding.GetEncoding(866); // You can also do: // Encoding := TEncoding.UTF8; // Encoding := TEncoding.Unicode; // Encoding := TEncoding.ANSI; try // Convert string (characters) to bytes Content := Encoding.GetBytes(S); finally FreeAndNil(Encoding); end; // Calculate hash from bytes Hash := CRC32Hash(Pointer(Content), Length(Content)); Finalize(Content); // optional // Show hash to a human Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash)); // Should be '6DB3A7B9' // You can also do IntToStr(Hash) - which will be 3114775405 end;
4. Check hash in PHP:
uses EEncoding, // for HexEncodeToString EHash, // for MD5Hash ECore; // for ShellExec procedure TForm1.Button1Click(Sender: TObject); var S: String; // Source string UTF8Str: UTF8String; // String's bytes Hash: TMD5Hash; // Hash (bytes) HashStr: String; // Hash (text) begin // Define source data S := 'Привет'; // Define exact representation as bytes // We use UTF-8 in this example UTF8Str := UTF8Encode(S); // (you can also just do UTF8Str := S; in Delphi 2009 and up) // Calculate hash from bytes Hash := MD5Hash(UTF8Str); // Convert bytes to text HashStr := HexEncodeToString(@Hash, SizeOf(Hash)); // Will be '8A669E9418750C81AB90AE159A8EC410' // Pass hash as text into PHP script ShellExec(Format('http://localhost/test.php?hash=%s', [HashStr])); end;
<?php // Source data (stored as UTF-8) $Source = 'Привет'; // Calculate hash of source (UTF-8 encoded) data // (the function will return text, not bytes) $Hash = md5($Source); // Read script's parameter $HashArg = $_GET['hash']; // Ensure both passed and calculated hashes match by comparing string representation if (strtolower($Hash) == strtolower($HashArg)) { // or you can do this starting with PHP 5.6: // if (hash_equals($HashArg, $Hash)) { echo('OK'); // we should get there, // e.g. source strings match // 'Привет' in Delphi = 'Привет' in PHP } else { echo('FAIL'); }
5. Storing user credentials in a database:
uses EHash, // for SHA256HMAC EEncrypt, // for InitSalt EEncoding; // for RAWToString and HexEncodeToString/HexDecodeFromString procedure TForm1.Button1Click(Sender: TObject); var UserName: String; // User's login (text) UserPassword: String; // User's password (text) UserPasswordRAW: RawByteString; // User's password (bytes) Salt: TSalt; // Salt (bytes) SaltStr: String; // Salt (text) Hash: TSHA256Hash; // Password's hash (bytes) Hash2: TSHA256Hash; // Hash from database (bytes) HashStr: String; // Password's hash (text) begin // Step 1. Create a new account // Obtain user's credentials somehow: UserName := InputBox('Sign in', 'Enter the login:', ''); UserPassword := InputBox('Sign in', 'Enter the password:', ''); // Create random bytes to be used as salt Salt := InitSalt; // Convert password (text) to bytes UserPasswordRAW := UTF8Encode(UserPassword); // Calculate hash from password (bytes) and salt (bytes) via HMAC Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW)); // Convert bytes to text SaltStr := HexEncodeToString(@Salt, SizeOf(Salt)); HashStr := HexEncodeToString(@Hash, SizeOf(Hash)); // Insert a new record into database // This is a pseudo-code InsertIntoDBTable('users', ['login', 'salt', 'password'], [UserName, SaltStr, HashStr]); // Here: // 'users' - table's name // 'login' - string field of arbitrary length // 'salt' - string field of 32 characters or binary field of 16 bytes // 'password' - string field of 64 characters or binary field of 32 bytes // Step 2. Authenticate a user // Obtain user's credentials somehow: UserName := InputBox('Log in', 'Enter the login:', ''); UserPassword := InputBox('Log in', 'Enter the password:', ''); // Search database for a user with the provided login // This is a pseudo-code // Real code should use "arguments" Query := Format('SELECT salt, password FROM users WHERE login = ''%s'' LIMIT 1', [UserName]); Values := DBQuery(Query); // If there is no DB entry - then the login is not correct if Length(Values) = 0 then begin ShowMessage('Invalid login'); Exit; end; // Convert salt and hash from text to bytes SaltStr := Values[0]; // 'salt' field from SELECT HashStr := Values[1]; // 'password' field from SELECT Assert(HexCalcDecodedSize(Length(SaltStr)) = SizeOf(Salt)); HexDecodeFromString(SaltStr, @Salt); Assert(HexCalcDecodedSize(Length(HashStr)) = SizeOf(Hash2)); HexDecodeFromString(HashStr, @Hash2); // Calculate hash in the same way as above UserPasswordRAW := UTF8Encode(UserPassword); Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW)); // Now we have: // Hash - hash (bytes) from the entered password // Hash2 - hash (bytes) from the database // If both hashes are equal - then password is correct // Ensure correct password by comparing hashes if CompareMem(@Hash, @Hash2, SizeOf(Hash)) then ShowMessage('OK') else ShowMessage('Invalid password'); end;
<?php // Step 1. Create a new account // Obtain user's credentials somehow: $UserName = $_GET['login']; $UserPassword = $_GET['password']; // Create random bytes to be used as salt $Salt = random_bytes(16); // Calculate hash from password (UTF-8 encoded) and salt (raw bytes) via HMAC $HashStr = hash_hmac('sha256', $Salt, $UserPassword); // Convert bytes to text $SaltStr = bin2hex($Salt); // Insert a new record into database // This is a pseudo-code InsertIntoDBTable('users', ['login', 'salt', 'password'], [$UserName, $SaltStr, $HashStr]); // Here: // 'users' - table's name // 'login' - string field of arbitrary length // 'salt' - string field of 32 characters or binary field of 16 bytes // 'password' - string field of 64 characters or binary field of 32 bytes // Step 2. Authenticate a user // Obtain user's credentials somehow: $UserName = $_GET['login']; $UserPassword = $_GET['password']; // Search database for a user with the provided login // This is a pseudo-code // Real code should use "arguments" $Query = 'SELECT salt, password FROM users WHERE login = \'' . $UserName . '\' LIMIT 1'; $Values = DBQuery($Query); // If there is no DB entry - then the login is not correct if (empty($Values)) { echo('Invalid login'); die; } // Convert salt and hash from text to bytes $SaltStr = Values['salt']; // 'salt' field from SELECT $HashStr2 = Values['password']; // 'password' field from SELECT $Salt = hex2bin($SaltStr); // Calculate hash in the same way as above $HashStr = hash_hmac('sha256', $Salt, $UserPassword); // Now we have: // $HashStr - hash (text) from the entered password // $HashStr2 - hash (text) from the database // If both hashes are equal - then password is correct // Ensure correct password by comparing hashes (as text) if (strtolower($HashStr) == strtolower($HashStr2)) { // or you can do this starting with PHP 5.6: // if (hash_equals($HashStr2, $HashStr)) { echo('OK'); // should get there, // e.g. password is correct } else { echo('Invalid password'); }
Encryption
EurekaLog has the following encryption functions:- In-process encryption (for example, to protect passwords)
- Cross-process encryption (for example, to protect stored data within user account, or even within whole PC)
- TEA
- Twofish
- RSA
Similar to hash functions, encryption functions also accepts pointer+size or
RawByteString
. However, encryption functions have to return data of arbitrary size too. That is why you can also use the TEncryptBuffer
record, which just combines pointer and its size into a single argument.In-process encryption
Sometimes an application has to operate on "secret" information (such as user's password). It is necessary to store such sensitive information in an encrypted form to reduce the leakage risks. You can read more about this practice in MSDN or (highly recommended) book. EurekaLog offers the following functions to protect sensitive information within a process:The
MemProtect
function encrypts the specified memory block within a process in a such a way that it can be decrypted back only from the same process. The MemUnprotect
function decrypts info back. The SecureFree
function could be used to securely dispose of almost anything. This function will wipe (erase) memory before freeing it.For example:
uses EEncrypt; // for MemProtect/MemUnprotect and SecureFree procedure TForm1.Button1Click(Sender: TObject); var UserPassword: String; StoredPassword: TEncryptBuffer; ClearText: TEncryptBuffer; begin // Prepare all buffers FillChar(StoredPassword, SizeOf(StoredPassword), 0); FillChar(ClearText, SizeOf(ClearText), 0); // Obtain some confidential info UserPassword := InputBox('Query', 'Enter the password:', ''); try // Encrypt info immediately ClearText.pbData := Pointer(UserPassword); ClearText.cbData := Length(UserPassword) * SizeOf(Char); MemProtect(ClearText, StoredPassword); finally // Wipe the confidential info SecureFree(UserPassword); // No need to dispose ClearText, // because we did not allocate memory for it end; // ... // Now we have StoredPassword - encrypted confidential info // We would need to decrypt it each time we want to use it // Don't forget to wipe unencrypted info once you have finished using it // ... // Decrypt the info: MemUnprotect(StoredPassword, ClearText); try // Use confidential info somehow Hash := MD5Hash(ClearText.pbData, ClearText.cbData); finally // Wipe unencrypted info after use SecureFree(ClearText); // You can also wipe derived info SecureFree(Hash); end; // ... // Clear (encrypted) confidential info when you no longer need it SecureFree(StoredPassword); end;
Cross-process encryption
Sometimes confidential information has to be stored somewhere. For example, a "Remember me" feature could write login/password pair into registry. TheMemProtect
/MemUnprotect
function will not help you in such cases, because those functions will not work between/across processes (e.g. app's restart means creating a new process). Therefore, EurekaLog offers similar functions: DataProtect
and DataUnprotect
. For example:uses EEncrypt, // for DataProtect/DataUnprotect and SecureFree EConfig, // for RegKeyWrite/RegKeyRead EEncoding; // for Base64EncodeString/Base64DecodeString procedure TForm1.Button1Click(Sender: TObject); var UserPassword: String; StoredPassword: RawByteString; ClearText: RawByteString; begin // Obtain some confidential info UserPassword := InputBox('Query', 'Enter the password:', ''); try // Convert to RawByteString for convenience ClearText := UTF8Encode(UserPassword); // Wipe the original form SecureFree(UserPassword); // Encrypt/protect the sensitive info StoredPassword := DataProtect(ClearText); // or: // StoredPassword := DataProtect(ClearText, True); // if you want to use HKEY_LOCAL_MACHINE below // Wipe the confidential info SecureFree(ClearText); // Convert encrypted bytes to text UserPassword := Base64EncodeString(StoredPassword); // Store encrypted confidential info into registry RegKeyWrite(HKEY_CURRENT_USER, '\SOFTWARE\YourApp', 'SavedPassword', UserPassword); // Optional SecureFree(UserPassword); finally // Just in case (e.g. exception) - wipe everything SecureFree(UserPassword); SecureFree(StoredPassword); SecureFree(ClearText); end; // ... // Now we have encrypted sensitive info stored in the registry // We need to read and decrypt it each time we want to use it // Don't forget to wipe it after usage // ... // Read (encrypted) confidential info from the registry UserPassword := RegKeyRead(HKEY_CURRENT_USER, '\SOFTWARE\YourApp', 'SavedPassword', ''); try // Convert text back to bytes StoredPassword := Base64DecodeString(UserPassword); // Optional SecureFree(UserPassword); // Decrypt confidential info ClearText := DataUnprotect(StoredPassword); // Optional SecureFree(StoredPassword); // Need to convert back to text UserPassword := UTF8ToString(ClearText); // Wipe the unsecured data SecureFree(ClearText); // Use sensitive info somehow Hash := MD5Hash(Pointer(UserPassword), Length(UserPassword) * SizeOf(Char)); // Wipe once finished SecureFree(UserPassword); finally // Just in case (e.g. exception) - wipe everything SecureFree(UserPassword); SecureFree(StoredPassword); SecureFree(ClearText); end; end;
Symmetric encryption
EurekaLog supports TEA and Twofish symmetric ciphers. Both ciphers are not patented and can be used in any app. TEA is used in a wide variety of hardware due to its extremely low memory requirements and ease of implementation. Twofish is a robust general purpose symmetric encryption algorithm.Note that encrypted data can be larger than the original data - since symmetric encryption algorithms often operate in blocks of data ("chunks"). A distinctive feature of TEA is that the encrypted data will be equal in size to the original data, so the TEA encryption/decryption functions have an overloaded option for in-place operations, i.e. without memory reallocation. For the Twofish algorithm, the data size must be a multiple of the block size (16 bytes) - otherwise the data will be padded with the PKCS#5 algorithm to a minimum required size.
EurekaLog offers algEncrypt/algDecrypt functions for both ciphers. All functions accepts key and source data. The difference is that Twofish functions allow you to additionally specify an optional IV (initialization vector). Initialization vector is just a set of random bytes (which you can create by calling the
TwofishInitIV
function), which ensures that two equal source data will look different after encryption, e.g. it is something like salt for hash. You don't need to store initialization vector securely, when used - it should be stored/transmitted together with encrypted data.Both cipers use binary keys (e.g. bytes) for encryption. Because cipers are "symmetric", this means that decryption key must be the same as encryption key. Obviosly, TEA key and Twofish key have different (but fixed) size. As a rule, encryption keys are not random, but are obtained from passwords entered by a user. To convert an arbitrary password into a key of a fixed length, the algDeriveKey functions are used, which accepts a block of data of an arbitrary size. Key is derived from a password by a simple call to the hash function with an appropriate size result. For example, for TEA it will be MD5, for Twofish it will be SHA-256. The derive functions also have an overload that accepts the password as a string with an optional salt. In this case, the password is converted to UTF-8 representation, and the hash is taken from the string 'salt' + 'UTF-8 password'. In addition, there is another version of derive functions: algDeriveKeyHMAC function, which uses the HMAC algorithm to combine salt and password. In general, if you plan to use salt - we recommend using the algDeriveKeyHMAC functions.
If the encryption keys are obtained in some other way (e.g. not from a password) - you can also exchange keys directly, without "deriving" them from passwords. Just treat the key as a fixed size record/bytes array. The only pitfall is that EurekaLog uses optimization with Twofish: the key is not used directly, but is first converted into an intermediate version, which allows optimizing encryption and decryption operations. The original key is named
TTwofishRAWKey
and the optimized version is TTwofishKey
.For example:
1. Encryption/decryption using a password:
uses EEncrypt; // for InitSalt, TEADeriveKey, TEAEncrypt/TEADecrypt, SecureFree procedure TForm1.Button1Click(Sender: TObject); var Salt: TSalt; // Salt (random bytes) Key: TTEAKey; // Key (derived from password) Source: String; // Source text SourceBytes: RawByteString; // Source text (bytes) EncryptedBytes: RawByteString; // Encrypted text begin // Prepare all buffers FillChar(Salt, SizeOf(Salt), 0); FillChar(Key, SizeOf(Key), 0); // Step 1: Encryption try // Define source data Source := 'Привет'; // Define exact representation as bytes // We use UTF-8 in this example SourceBytes := UTF8Encode(Source); // No need source data anywore - wipe it SecureFree(Source); // Generate random bytes to be used as salt Salt := InitSalt; // Derive key from password and salt Key := TEADeriveKeyHMAC('a super secret password', Salt); // Encrypt source data EncryptedBytes := TEAEncrypt(Key, SourceBytes); // Wipe everything that we no longer need SecureFree(Key); SecureFree(SourceBytes); finally // Just in case (e.g. exception) - wipe everything SecureFree(Source); SecureFree(SourceBytes); SecureFree(Key); end; // How we have: // Salt - salt to derive key from password // EncryptedBytes - encrypted data (arbitrary size) // Both of those should be transferred to decryption side // Step 2: Decryption try // Derive key from password and salt (salt should be passed to us) Key := TEADeriveKeyHMAC('a super secret password', Salt); // Now Key must match encryption key exactly // Decrypt encrypted data SourceBytes := TEADecrypt(Key, EncryptedBytes); // Wipe key SecureFree(Key); // Optional SecureFree(EncryptedBytes); // Convert data back to text Source := UTF8ToString(SourceBytes); // Wipe unneeded data SecureFree(SourceBytes); // Use source data somehow ShowMessage(Source); // Wipe source data after usage SecureFree(Source); finally // Just in case (e.g. exception) - wipe everything SecureFree(Source); SecureFree(SourceBytes); SecureFree(EncryptedBytes); SecureFree(Key); SecureFree(Salt); end; end;
2. Exchange encrypted data between Delphi and PHP:
uses EEncrypt, // for all Twofish functions and SecureFree EEncoding, // for Base64EncodeToString ECore; // for ShellExec procedure TForm1.Button1Click(Sender: TObject); const // Secret key known to both parties // It must match key in PHP-script exactly // Basically, this is just random bytes, which you can generate via TwofishInitSessionKeyRAW // Obviosly, if you are going to use this example - you MUST replace this constant // This is just an example. A real app may store this key somewhere SecretKey: TTwofishRAWKey = (160, 22, 228, 9, 73, 192, 173, 149, 154, 19, 115, 215, 74, 36, 20, 202, 178, 26, 103 , 47, 51, 4, 144, 20, 73, 153, 49, 160, 192, 25, 20, 114); var Key: TTwofishKey; // Optimized key (created from constant above) IV: TTwofishInitVector; // Initialization vector (bytes) Text: String; // Source data (text) TextRAW: RawByteString; // Source data (bytes) EncryptedText: RawByteString; // Encrypted data (bytes) EncodedIV: String; // Initialization vector (text) EncodedText: String; // Encrypted data (text) URL: String; // URL to call PHP-script ReplyRAW: RawByteString; // Reply from PHP-script (bytes) Reply: String; // Reply from PHP-script (text) begin // Prepare buffers FillChar(Key, SizeOf(Key), 0); FillChar(IV, SizeOf(IV), 0); try // Obtain source data somehow Text := 'Привет!'; // Convert text to bytes. Use UTF-8 in this example TextRAW := UTF8Encode(Text); // No longer needed SecureFree(Text); // Prepare (optimize) the key Key := TwofishInitKey(SecretKey); // Generate random bytes to be used as initialization vector IV := TwofishInitIV; // Encrypt source data // This will use CBC mode, because initialization vector is used EncryptedText := TwofishEncrypt(Key, TextRAW, @IV); // Wipe source text SecureFree(TextRAW); // Convert bytes to text EncodedIV := Base64EncodeToString(@IV, SizeOf(IV)); EncodedText := Base64EncodeString(EncryptedText); // Optional SecureFree(EncryptedText); // Crearte URL to call PHP-script // URLEncode is required to escape the '+' character // If you are going to use HEX-encoding instead of Base64 - then URLEncode is not required URL := Format('http://localhost/test.php?iv=%s&text=%s', [URLEncode(EncodedIV), URLEncode(EncodedText)]); // Optional SecureFree(EncodedIV); SecureFree(EncodedText); // Call PHP-script if not InitWebTools then RaiseLastOSError; try ReplyRAW := InternetGet(URL, [], []); finally DoneWebTools; end; // PHP-script did not returned anything? if ReplyRAW = '' then Abort; // Optional SecureFree(URL); // Convert text to bytes ReplyRAW := Base64DecodeString(Trim(String(ReplyRAW))); // Decrypt data TextRAW := TwofishDecrypt(Key, ReplyRAW, @IV); // No longer needed SecureFree(Key); SecureFree(IV); // Optional SecureFree(ReplyRAW); // Convert bytes back to text Text := UTF8ToString(TextRAW); // No longer needed SecureFree(TextRAW); // Use reply from PHP-script ShowMessage(Text); // Should show: // 'Hello from PHP: Привет' // Wipe the reply after use SecureFree(Text); finally // Just in case (e.g. exception) - wipe everything SecureFree(Text); SecureFree(TextRAW); SecureFree(Key); SecureFree(IV); SecureFree(EncryptedText); SecureFree(EncodedIV); SecureFree(EncodedText); SecureFree(URL); SecureFree(ReplyRAW); SecureFree(Reply); end; end;
<?php // The functions below are required, because MCrypt use zero-padding instead of PKCS#5 // While OpenSSL does support PKCS#5, but it does not support Twofish // PKCS#5 padding function pkcs5_pad($text, $blocksize = 16) { $pad = $blocksize - (strlen($text) % $blocksize); return $text . str_repeat(chr($pad), $pad); } // PKCS#5 removal (unpadding) function pkcs5_unpad($text) { $pad = ord($text{strlen($text)-1}); if ($pad > strlen($text)) { return false; } if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) { return false; } return substr($text, 0, -1 * $pad); } // Secret key known to both parties // It must match key in Delphi exactly // Basically, this is just random bytes, which you can generate via random_bytes(32) // Obviosly, if you are going to use this example - you MUST replace this constant // This is just an example. A real app may store this key somewhere $Key = pack('C*', 160, 22, 228, 9, 73, 192, 173, 149, 154, 19, 115, 215, 74, 36, 20, 202, 178, 26, 103 , 47, 51, 4, 144, 20, 73, 153, 49, 160, 192, 25, 20, 114); // Read passed data (initialization vector and encrypted data) $EncodedIV = $_GET['iv']; $EncodedText = $_GET['text']; // Convert text to bytes $IV = base64_decode($EncodedIV); $EncryptedText = base64_decode($EncodedText); // Decrypt data (you must use MCRYPT_TWOFISH256 instead of MCRYPT_TWOFISH in some versions of PHP) $Text = mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedText, MCRYPT_MODE_CBC, $IV); // Trim data to its real size $Text = pkcs5_unpad($Text); // Do something with source data from Delphi $Text = 'Hello from PHP: ' . $Text; // Pad data up to chunk border $Text = pkcs5_pad($Text); // Encrypt source data (you must use MCRYPT_TWOFISH256 instead of MCRYPT_TWOFISH in some versions of PHP) $EncryptedText = mcrypt_encrypt(MCRYPT_TWOFISH, $Key, $Text, MCRYPT_MODE_CBC, $IV); // Convert bytes to text $EncodedText = base64_encode($EncryptedText); // Send encrypted data back to Delphi echo($EncodedText);
3. Storing file encrypted:
uses EEncrypt, // for all Twofish functions and SecureFree EEncoding, // for HexEncodeToString/HexDecodeString EConfig, // for RegKeyWrite/RegKeyRead ECompatibility; // for FileToString/StringToFile procedure TForm1.Button1Click(Sender: TObject); var RAWKey: TTwofishRAWKey; // Encryption/decryption (session) key (RAW bytes) Key: TTwofishKey; // Encryption/decryption (session) key (optimized) IV: TTwofishInitVector; // Initialization vector Content: RawByteString; // Source file (bytes) EncryptedData: RawByteString; // Encrypted file DataClear: RawByteString; // To encrypt encryption/session key DataEncrypted: RawByteString; // To encrypt encryption/session key DataStr: String; // Encrypted encryption/session key (text) begin // Prepare all buffers FillChar(RAWKey, SizeOf(RAWKey), 0); FillChar(Key, SizeOf(Key), 0); FillChar(IV, SizeOf(IV), 0); // Step 1: encrypt file on disk with random key try // Create random key (random bytes) RAWKey := TwofishInitSessionKeyRAW; // Optimize it for further use Key := TwofishInitKey(RAWKey); // Load whole file into a string Content := FileToString(ParamStr(0)); // Content will be like 'MZP'#0#2#0#0#0... // Use random bytes as initialization vector IV := TwofishInitIV; // Encrypt data (e.g. file) EncryptedData := TwofishEncrypt(Key, Content, @IV); // Wipe not needed data SecureFree(Content); SecureFree(Key); finally // Just in case (e.g. exception) - wipe everything SecureFree(Content); SecureFree(Key); end; // Now we have: // RAWKey - (secret) session key, which was used to encrypt the file // IV - (open) initialization vector // EncryptedData - encrypted file // Step 2: protect session key try // Convert to RawByteString for convenience DataClear := RAWToString(@RAWKey, SizeOf(RAWKey)); // Wipe key (no longer needed) SecureFree(RAWKey); // Encrypt encryption key DataEncrypted := DataProtect(DataClear); // Wipe unneccessary data SecureFree(DataClear); // Convert bytes to text DataStr := HexEncodeToString(Pointer(DataEncrypted), Length(DataEncrypted)); // Optional SecureFree(DataEncrypted); // Store encrypted session key into registry // We can safely do that, because this key can be decrypted back by the same user account only RegKeyWrite(HKEY_CURRENT_USER, '\SOFTWARE\YourApp', 'EncryptionKey', DataStr); // Optional SecureFree(DataStr); finally // Just in case (e.g. exception) - wipe everything SecureFree(RAWKey); SecureFree(DataClear); SecureFree(DataEncrypted); SecureFree(DataStr); end; // Now we have: // Encrypted (secret) session (encryption) key in the registry // IV - (open) initialization vector // EncryptedData - encrypted file // So, if we want to save encrypted file to disk - we need to save both IV and EncryptedData // Only current user account would be able to decrypt data back // Step 3: decrypt session key try // Read stored session key (as text) DataStr := RegKeyRead(HKEY_CURRENT_USER, '\SOFTWARE\YourApp', 'EncryptionKey', ''); // Convert text to bytes DataEncrypted := HexDecodeString(DataStr); // Optional SecureFree(DataStr); // Decrypt session key DataClear := DataUnprotect(DataEncrypted); // Optional SecureFree(DataEncrypted); // Prepare session key (bytes) Assert(Length(DataClear) = SizeOf(RAWKey)); RAWFromString(DataClear, @RAWKey); // Wipe it's copy SecureFree(DataClear); finally // Just in case (e.g. exception) - wipe everything SecureFree(DataStr); SecureFree(DataEncrypted); SecureFree(DataClear); end; // Now we have: // RAWKey - (secret) key, which was used to encrypt the file // IV - (open) initialization vector // EncryptedData - encrypted file // Step 4: decrypt the file try // Optimize the key for usage Key := TwofishInitKey(RAWKey); // Wipe source (no longer needed) SecureFree(RAWKey); // Decrypt the data (file) // Both Key and IV must be exactly the same as in encryption at step 1 above Content := TwofishDecrypt(Key, EncryptedData, @IV); // Wipe the key (no longer needed) SecureFree(Key); // Optional SecureFree(EncryptedData); // Store unencrypted data back to file StringToFile(ParamStr(0) + '.copy', Content); // Now the Project1.exe.copy file must be exact copy of Project1.exe // Wipe unnecessary data SecureFree(Content); finally // Just in case (e.g. exception) - wipe everything SecureFree(RAWKey); SecureFree(Key); SecureFree(EncryptedData); SecureFree(Content); end; end;
Asymmetric Cryptography
For asymmetric encryption - EurekaLog supports RSA. The algorithm is not proprietary and can be freely used in any app. The asymmetry of the algorithm means that it uses two different keys: one key is used for encryption, the other key is used for decryption. One of the keys is kept secret, it is called "secret" or "private" key, and the other can be published - it is called "public" or "open" key. Whatever is encrypted with the public key can only be decrypted back with the matched private key and vice versa.Key Management
EurekaLog stores RSA keys into theTRSAKey
record, which have two fields: PublicKey
to store public key and PrivateKey
to store private key. You can create a brand new pair of keys by calling the RSAGenKey
function (which will take a while - say, 5-15 seconds). As a rule, keys are not generated in apps, but ready-made (pre-generated) keys are loaded. EurekaLog offers the RSALoad/SavePublic/PrivateKey
functions to load and save keys, for example: RSALoadPrivateKey
. EurekaLog supports few formats to export/import the keys, which are described by the TRSAExport/TRSAImport
type:
rsBLOB
- this is a binary representation of a key with a header. ThePUBLICKEYBLOB/PRIVATEKEYBLOB
record from Microsoft is used as the header. Little-Endian.rsDER
- this is a binary representation of a key, encoded into ASN.1 container without any headers (so called PKCS#1), Big-Endian. If you attempt to load ASN.1 container with a header (PKCS#8) - it will be silently ignored. It matches theRSA_CSP_PUBLICKEYBLOB/PKCS_RSA_PRIVATE_KEY
in CryptoAPI. As a rule, this format is used to save into.der
files.rsPEM
- this is a textual representation of a key. Basically, it is the samersDER
, but encoded into Base64. As a rule, this format is used to save into.pem
files..key
,.cert
,.cer
, or.crt
are also used. PKCS#1 compliant files use headers like-----BEGIN/END RSA PRIVATE KEY-----
,-----BEGIN/END RSA PUBLIC KEY-----
, while PKCS#8 complient files use headers like-----BEGIN/END PRIVATE KEY-----
,-----BEGIN/END PUBLIC KEY-----
.
For example, the very same key can be exported like the following:
rsBLOB
(thePUBLICKEYBLOB/PRIVATEKEYBLOB
header + little-endian, key $25 $17 $B4 $A0 ... $96 $B9 $9C $E7 starts from byte $15/21 and ends at the file's end):
rsDER
(ASN.1-container PKCS#1 + big-endian, e.g. the same key $E7 $9C $B9 $96 ... $A0 $B4 $17 $25 starts with byte 10 and ends at byte 6 from file's end):
rsPEM
(Base64-encoded ASN.1-container PKCS#1):
-----BEGIN RSA PUBLIC KEY----- MIICCgKCAgEA55y5ll1KryRC7umxntWX7t3zOP3qUVxQo7gin3sA1dePyzLxTxtE 47R+/sqkgFygXdlBqnmjbwu60kU2Zd7k7QFGhZWqfPcAYI3xd660vUPnmXK7n2R1 3AtF2BW/5MqIH7D3ddjLCt5CoUn6KRZSuz+pySDpuquKerRB5Gq/0WjUG2IIcQXU Z1i4qMicPhbOJH76rFPgRngBuvJtS0UCBKx4YOlK0q1JUUJ1leSGp2gAjYGrD7fN SOU8r70a97NDu4UblmsS9zW29OHAEF7jNFsVNVBU78P/XZ4hmL41gaPRGws3HXfA vGbVattUzHTHsHMJeRLoiPAgak3TqAM2px7qOcNNN8FB91XbnxzvPARfDrBMbpc4 OcWmDSMuc1RGI/mQCIlGRvA2nhD7Dfu3L5sxnrjjOC+LLpIVsGe5+cs1ZkfD7kII AzV/MXXNlx366n/Z1+u97VocmvHcqVCl/s9AMqdXflzAYD+9p7bXhJdP9XfOXf9z zCyPBK/Iyk+B4lRR9cmuBW7FAq1JM3PZWZ2mEx0fgrL8M0w5cf2Ts84XtNIEFDa6 MFOe48sJfDIiPPw4ePohSuYpAY71Du2cQe87VQAf/caclWsrFplItilN93Xx5kQW 5S16HHLc7A+EKEaNBnUsNl+n0/99jjfHA9PAqxFVaVT68X9eSKC0FyUCAwEAAQ== -----END RSA PUBLIC KEY-----
Creating and exporting a new key pair:
- Delphi (PKCS#1):
uses EEncrypt; // for all RSA functions procedure TForm1.Button1Click(Sender: TObject); var Key: TRSAKey; begin // Create a new key pair (takes few seconds) Key := RSAGenKey; try // Export both keys into all formats (only as an example) RSASavePublicKey(Key, 'C:\Documents\public.blob', rsBLOB); RSASavePublicKey(Key, 'C:\Documents\public.der', rsDER); RSASavePublicKey(Key, 'C:\Documents\public.pem', rsPEM); RSASavePrivateKey(Key, 'C:\Documents\private.blob', rsBLOB); RSASavePrivateKey(Key, 'C:\Documents\private.der', rsDER); RSASavePrivateKey(Key, 'C:\Documents\private.pem', rsPEM); finally // Wipe all keys SecureFree(Key); end; end;
- PHP (PKCS#8):
<?php // Create a new key pair (takes few seconds) $config = array( "private_key_bits" => 4096, "private_key_type" => OPENSSL_KEYTYPE_RSA, "encrypt_key" => false ); $privateKey = openssl_pkey_new($config); $publicKey = openssl_pkey_get_public($privateKey); // Save keys into files openssl_pkey_export($privateKey, $PEM, null, $config); file_put_contents('./private.pem', $PEM); file_put_contents('./public.pem', $publicKey['key']);
- OpenSSL (PKCS#8):
openssl genpkey -out private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:4096 openssl rsa -in private.pem -out public.pem -outform pem -pubout
If you want to exchange keys between Delphi/EurekaLog, Windows/WinCrypt, and PHP/OpenSSL - you need to use an ASN.1-container format (i.e. binary DER or text PEM). There is one pitfall here:
- Windows/WinCrypt exports/imports only keys themselfes into ASN.1-container. Such format is called PKCS#1, you can distinguish it in a text form (PEM) by the comments like
-----BEGIN RSA PUBLIC KEY-----
. If you open such PEM/DER file into any ASN.1 editor/viewer/decoder (or use theopenssl asn1parse -in private.pem
command) - you should see something like this:SEQUENCE (9 elem) INTEGER 0 INTEGER (4096 bit) 758666102228921792910938751013886686183781000609266742264329283135491… INTEGER 65537 INTEGER (4096 bit) 626038158727742962915964533848528190012037116635627896022932505790124… INTEGER (2048 bit) 265173868639039499374763037151257212413358779720904507462546669893261… INTEGER (2048 bit) 286101381754789259717271171537486143896554619163648315398099842455950… INTEGER (2048 bit) 241273140164272477344356926702923044634459679795497746005945617372402… INTEGER (2047 bit) 117977475958972484914769571551956345862709440207784850140129213152449… INTEGER (2047 bit) 114103066753170488160676998988478007480145103422326665392933549037964…
8 feilds describe various components of the key (module, exponent, etc.). - PHP/OpenSSL exports not just keys, but also stores additional information, such as alg ID, version, etc. Such format is called PKCS#8, you can distinguish it in a text form (PEM) by the comments like
-----BEGIN PRIVATE KEY-----
. If you open such PEM/DER file into any ASN.1 editor/viewer/decoder - you should see something like this:SEQUENCE (3 elem) INTEGER 0 SEQUENCE (2 elem) OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) NULL OCTET STRING (2349 byte) 30820929020100028202010098D61F2FBA3EC958DB082F286781EE7CC258ADCE2B0A… SEQUENCE (9 elem) INTEGER 0 INTEGER (4096 bit) 758666102228921792910938751013886686183781000609266742264329283135491… INTEGER 65537 INTEGER (4096 bit) 626038158727742962915964533848528190012037116635627896022932505790124… INTEGER (2048 bit) 265173868639039499374763037151257212413358779720904507462546669893261… INTEGER (2048 bit) 286101381754789259717271171537486143896554619163648315398099842455950… INTEGER (2048 bit) 241273140164272477344356926702923044634459679795497746005945617372402… INTEGER (2047 bit) 117977475958972484914769571551956345862709440207784850140129213152449… INTEGER (2047 bit) 114103066753170488160676998988478007480145103422326665392933549037964…
The key is stored in the same way as in PKCS#1, but there is also an additional header added.
CRYPT_E_ASN1_BADTAG (8009310B): ASN1 bad tag value met
openssl_pkey_get_private: error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag
EurekaLog saves keys into PKCS#1, but it is able to load both PKCS#1 and PKCS#8. OpenSSL saves into PKCS#8, but it supports loading both PKCS#8 and PKCS#1.
IMPORTANT
OpenSSL supports PKCS#1/PKCS#8 PEM only when it has the proper comment! E.g. the PKCS#1 file must start with the-----BEGIN RSA PRIVATE KEY-----
or-----BEGIN RSA PUBLIC KEY-----
, while PKCS#8 file must start with the-----BEGIN PRIVATE KEY-----
or-----BEGIN PUBLIC KEY-----
.
P.S. You can also convert PKCS#1 (Windows/WinCrypt/EurekaLog) into PKCS#8 (PHP/OpenSSL) via:
openssl pkcs8 -topk8 -inform pem -in private.pem -outform pem -nocrypt -out private2.pem openssl rsa -RSAPublicKey_in -in public.pem -pubout -out public2.pemand back:
openssl rsa -inform pem -in private.pem -outform pem -out private2.pem openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public2.pem
P.P.S. Latest versions of OpenSSL supports BLOB:
Convert PEM to BLOB:
openssl rsa -inform PEM -in private.pem -outform "MS PRIVATEKEYBLOB" -out private.blobConvert BLOB to PEM:
openssl rsa -inform "MS PRIVATEKEYBLOB" -in private.blob -outform PEM -out private.pemChange key format to
"MS PUBLICKEYBLOB"
for a public key, or you can simply convert private key, then extract public key from the private key.In summary, we recommend:
- Use the EurekaLog Crypto Helper tool to create a pair of new RSA keys:
- Run Start / Programs / EurekaLog / Tools / EurekaLog Crypto Helper
- Go to the Keys tab
- Go to the RSA tab
- Hit Create New button
- Save private and public keys into
private.pem
andpublic.pem
files. This will save keys into PKCS#1.
uses EEncrypt; procedure TForm1.Button1Click(Sender: TObject); var Key: TRSAKey; begin Key := RSAGenKey; try RSASavePublicKey (Key, 'C:\Documents\public.pem', rsPEM); RSASavePrivateKey(Key, 'C:\Documents\private.pem', rsPEM); finally SecureFree(Key); end; end;
- Import keys into Delphi (EurekaLog supports PKCS#1):
var RSAKey: TRSAKey; begin // Loads PKCS#1 PEM RSAKey := RSALoadPrivateKey('C:\Documents\private.pem', rsPEM);
var RSAKey: TRSAKey; begin // Loads PKCS#1 PEM RSAKey := RSALoadPublicKey('C:\Documents\public.pem', rsPEM);
- Import keys into PHP (OpenSSL from PHP supports PKCS#1):
<?php $PrivateKey = <<<EOD -----BEGIN RSA PRIVATE KEY----- MIIJKgIBAAKCAgEA5CBBmb8QlDVt7uqKZPW6I/GcjyzDg+7VuZd5OcQXVpglsWoa ... aNz1gCLcqrQiTXHTVg821kYszBDySjfQGJQ3JJhf1/9XGcVjcopbWWeeNpHs5w== -----END RSA PRIVATE KEY----- EOD; // Or: // $PrivateKey = 'file:///var/www/private.pem'; $PrivateKey = openssl_pkey_get_private($PrivateKey); if (!PrivateKey) { echo('openssl_pkey_get_private: ' . openssl_error_string()); die(); }
<?php $PublicKey = <<<EOD -----BEGIN RSA PUBLIC KEY----- MIICCgKCAgEA5CBBmb8QlDVt7uqKZPW6I/GcjyzDg+7VuZd5OcQXVpglsWoauYvq ... f69nl8KyfHhsqffkDeDIaA73hspgFM5bh2zGdj4n8101bjHRu8N35qECAwEAAQ== -----END RSA PUBLIC KEY----- EOD; // Or: // $PublicKey = 'file:///var/www/public.pem'; $PublicKey = openssl_pkey_get_public($PublicKey); if (!PublicKey) { echo('openssl_pkey_get_public: ' . openssl_error_string()); die(); }
It is important that keys must start with a correct comment. E.g.-----BEGIN RSA PRIVATE KEY-----
and-----BEGIN RSA PUBLIC KEY-----
.
- Download OpenSSL for Windows.
- Create a new key pair. The commands below will create two files (
private.pem
andpublic.pem
) as PKCS#8 PEM:
openssl genpkey -out private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:4096 openssl rsa -in private.pem -out public.pem -outform pem -pubout
- Import keys in Delphi (EurekaLog supports PKCS#8):
var RSAKey: TRSAKey; begin // Loads PKCS#8 PEM RSAKey := RSALoadPrivateKey('C:\Documents\private.pem', rsPEM);
var RSAKey: TRSAKey; begin // Loads PKCS#8 PEM RSAKey := RSALoadPublicKey('C:\Documents\public.pem', rsPEM);
- Import keys in PHP (OpenSSL in PHP supports PKCS#8):
<?php $PrivateKey = <<<EOD -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDkIEGZvxCUNW3u ... X+VZcAA+CUNp1xYUuep0UoeqYtfzTuKvinNo3PWAItyqtCJNcdNWDzbWRizMEPJK N9AYlDckmF/X/1cZxWNyiltZZ542kezn -----END PRIVATE KEY----- EOD; // Or: // $PrivateKey = 'file:///var/www/private.pem'; $PrivateKey = openssl_pkey_get_private($PrivateKey); if (!PrivateKey) { echo('openssl_pkey_get_private: ' . openssl_error_string()); die(); }
<?php $PublicKey = <<<EOD -----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5CBBmb8QlDVt7uqKZPW6 ... v4sIP6NMmNKN8TwtqUKxcjZJMrVhjPJWf69nl8KyfHhsqffkDeDIaA73hspgFM5b h2zGdj4n8101bjHRu8N35qECAwEAAQ== -----END PUBLIC KEY----- EOD; // Or: // $PublicKey = 'file:///var/www/public.pem'; $PublicKey = openssl_pkey_get_public($PublicKey); if (!PublicKey) { echo('openssl_pkey_get_public: ' . openssl_error_string()); die(); }
It is important that keys must start with a correct comment. E.g.-----BEGIN PRIVATE KEY-----
and-----BEGIN PUBLIC KEY-----
.
Note: the
openssl_pkey_get_private
/openssl_pkey_get_public
calls are optional. If the key variable contains the key itself in PEM format, then it can be passed directly to OpenSSL functions. In this case, the openssl_pkey_get_private
/openssl_pkey_get_public
functions are called only as an example and to check that the key was specified correctly.Asymmetric Encryption
EurekaLog offers theRSAEncrypt
and RSADecrypt
functions, which are used in a similar way to the symmetric encryption functions above. There are only a few differences:
- Because asymmetric encryption uses two different keys, it is used in sender-recipient scenarios and is not used when the same person encrypts and decrypts data.
- Since asymmetric encryption is very slow, it is never applied to the open data itself. Instead, the open data is encrypted with any symmetric cipher with a random key (called a "session key"), and then the symmetric session key is encrypted with asymmetric encryption.
- The public key is used for encryption, and the private key is used for decryption. Therefore anyone can encrypt data with the recipient's public key, while only the recipient can decrypt the data. This is how secrecy is ensured.
RSAEncrypt
/RSADecrypt
functions work with little-endian data and use PKCS#1 Type 2 padding.Encrypting file on a disk:
uses EEncrypt; // for all RSA functions and SecureFree procedure TForm1.Button1Click(Sender: TObject); var // (Random) session key to encrypt the file SessionKey: TTwofishKey; SessionKeyRAW: TTwofishRAWKey; // Asymmetric key to encrypt the session key RSAKey: TRSAKey; // Open data (file) to encrypt Data: TMemoryStream; // Encrypted data EncryptedData: TEncryptBuffer; // Stream to save the encrypted file FS: TFileStream; begin // Prepare all buffers FillChar(SessionKey, SizeOf(SessionKey), 0); FillChar(SessionKeyRAW, SizeOf(SessionKeyRAW), 0); FillChar(RSAKey, SizeOf(RSAKey), 0); FillChar(EncryptedData, SizeOf(EncryptedData), 0); try // Create a new (random) session key SessionKeyRAW := TwofishInitSessionKeyRAW; SessionKey := TwofishInitKey(SessionKeyRAW); // FS is used twice: // 1). To store encrypted session key // 2). To store encrypted data FS := nil; try // Step 1: encrypt session key // Load public key from a file (must be prepared beforehand) RSAKey := RSALoadPublicKey('C:\Documents\public.pem', rsPEM); try // Encrypt session key (SessionKeyRAW) into EncryptedData buffer EncryptedData.cbData := SizeOf(SessionKeyRAW); RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData); finally // No longer need public key SecureFree(RSAKey); // No longer need source for the session key SecureFree(SessionKeyRAW); end; try // Now: EncryptedData stored encrypted session key // Save encrypted session key into destination file FS := TFileStream.Create('C:\Documents\EncryptedData.bin', fmCreate or fmShareExclusive); FS.WriteBuffer(EncryptedData.cbData, SizeOf(EncryptedData.cbData)); FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData); // Don't close FS yet, we are not finished saving data... finally // Wipe encrypted session key SecureFree(EncryptedData); end; // Now we have SessionKey and destination file FS // Step 2: encrypt the file Data := TMemoryStream.Create; try // Load entire file into memory Data.LoadFromFile('C:\Documents\Text.txt'); // Encrypt the file // We are not using initialization vector, so ECB mode will be used EncryptedData.cbData := Cardinal(Data.Size); TwofishEncrypt(SessionKey, Data.Memory, Pointer(EncryptedData.pbData), EncryptedData.cbData); // No longer needed SecureFree(SessionKey); // Now save encrypted file into destination file FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData); finally // Wipe everything SecureFree(Data); SecureFree(EncryptedData); end; finally // Close the file FreeAndNil(FS); end; // Now the C:\Documents\EncryptedData.bin file is ready // It contains encrypted (random) session key, as well as encrypted C:\Documents\Text.txt file // This file can be descrypted by someone, who has the private key // E.g. you can safely pass this file to the recipient by any means, including unsecured channels finally // Wipe the remaining data SecureFree(SessionKey); end; end; procedure TForm1.Button2Click(Sender: TObject); var // (Random) session key to decrypt the file SessionKey: TTwofishKey; // Asymmetric key to decrypt the session key RSAKey: TRSAKey; // Encrypted file Data: TMemoryStream; // Decrypted file DecryptedData: TEncryptBuffer; // Stream to load the encrypted file FS: TFileStream; begin // Prepare all buffers FillChar(SessionKey, SizeOf(SessionKey), 0); FillChar(RSAKey, SizeOf(RSAKey), 0); FillChar(DecryptedData, SizeOf(DecryptedData), 0); // Open encrypted file FS := TFileStream.Create('C:\Documents\EncryptedData.bin', fmOpenRead or fmShareDenyWrite); try try // Step 1: decrypt the session key // Read encrypted session key from the file FS.ReadBuffer(DecryptedData.cbData, SizeOf(DecryptedData.cbData)); DecryptedData.pbData := AllocMem(DecryptedData.cbData); try FS.ReadBuffer(DecryptedData.pbData^, DecryptedData.cbData); // Decrypt symmetric session key with private asymmetric key RSAKey := RSALoadPrivateKey('C:\Documents\private.pem', rsPEM); try EEncrypt.RSADecrypt(RSAKey, DecryptedData); finally SecureFree(RSAKey); end; // Initialize the session key Assert(DecryptedData.cbData = SizeOf(TTwofishRAWKey)); SessionKey := TwofishInitKey(TTwofishRAWKey(Pointer(DecryptedData.pbData)^)); finally // Wipe unneeded data SecureFree(DecryptedData); end; // Now we have symmetric SessionKey, which we can use to decrypt the rest of the file // Step 2: decrypt the file Data := TMemoryStream.Create; try // The remaining data in the file represents the encrypted file Data.CopyFrom(FS, FS.Size - FS.Position); // Decrypt the file DecryptedData.cbData := Cardinal(Data.Size); TwofishDecrypt(SessionKey, Data.Memory, Pointer(DecryptedData.pbData), DecryptedData.cbData); // No longer needed SecureFree(SessionKey); // Save decrypted file to disk FreeAndNil(FS); FS := TFileStream.Create('C:\Documents\Text2.txt', fmCreate or fmShareExclusive); try FS.WriteBuffer(DecryptedData.pbData^, DecryptedData.cbData); finally FreeAndNil(FS); end; // No longer needed SecureFree(DecryptedData); // Now Text2.txt should be exact copy of Text.txt finally SecureFree(Data); end; finally // Just in case (e.g. exception) - wipe everything SecureFree(SessionKey); SecureFree(DecryptedData); end; finally FreeAndNil(FS); end; end;
Digital Signature
When a private key is used for encryption and a public key is used for decryption - it is called a "digital signature". Anyone can decrypt the data (since everyone has the public key), therefore secrecy of data is not ensured in this way. But on the other hand, if the data can be decrypted with someone's public key, we can be sure that the data was encrypted by him (because only this person has the secret private key). This way we can check authenticity of the data.However, as a rule, data itself is not encrypted by private key. A hash is calculated from the data and the hash is then encrypted instead. EurekaLog offers
RSASign
(uses private key) and RSAVerify
(uses public key) functions to sign and verify. EurekaLog's digital signature functions use SHA1 with EMSA-PKCS1 padding.The resulting digital signature is an opaque array of bytes of arbitrary length. If you want to exchange digital signatures with other environments, remember that Windows/Delphi use little-endian byte order, while some other environments (e.g. .NET or PHP) use big-endian. Therefore, in some cases, the byte order of a digital signature needs to be reversed.
For example:
Requesting software license from a PHP-script:
uses EEncrypt, // for all RSA functions and SecureFree EEncoding, // for Base64 EJSON, // for JSON functions EWebTools; // for network functions procedure TForm1.Button1Click(Sender: TObject); var // Data to be send to a PHP-script JSON, JSONRequest, JSONUser: IJSONValues; JSONText: String; JSONRAW: RawByteString; // (Random) symmetric key to encrypt the data SessionKey: TTwofishKey; SessionKeyRAW: TTwofishRAWKey; // Asymmetric key to encrypt the session key RSAKey: TRSAKey; // Encrypted data (bytes) EncryptedData: TEncryptBuffer; // Encrypted data (text) EncodedKey, EncodedData: String; // URL to call PHP-script URL: String; // Reply from PHP (bytes) ReplyRAW: RawByteString; // License (text) EncodedLicense: String; // Digital signature (text) EncodedSignature: String; // License (bytes) License: RawByteString; // Digital signature (bytes) Signature: RawByteString; begin // Prepare all buffers FillChar(SessionKey, SizeOf(SessionKey), 0); FillChar(SessionKeyRAW, SizeOf(SessionKeyRAW), 0); FillChar(RSAKey, SizeOf(RSAKey), 0); FillChar(EncryptedData, SizeOf(EncryptedData), 0); try // Step 1: prepare some data to send to a PHP-script // The code below is just an example JSON := JSONCreate; JSONRequest := JSONCreate; JSONUser := JSONCreate; JSONRequest['version'] := 1; JSONRequest['type'] := 'license'; JSONRequest['scope'] := 'installer'; JSONUser['login'] := 'input-from-edit1'; JSONUser['password'] := 'input-from-edit2'; JSON['app'] := 'MyApp'; JSON['version'] := GetModuleVersion(GetModuleName(HInstance)); JSON['date'] := Now; JSON['request'] := JSONRequest; JSON['user'] := JSONUser; JSONText := JSON.ToString; Finalize(JSONUser); // optional Finalize(JSONRequest); // optional Finalize(JSON); // optional (* Now JSONText contains: { "app": "MyApp", "version": "1.0.0.0", "date": "2021.06.25 14:04:21", "request": { "version": 1, "type": "license", "scope": "installer" } "user": { "login": "input-from-edit1", "password": "input-from-edit2" } } *) JSONRAW := UTF8Encode(JSONText); SecureFree(JSONText); // Now JSONRAW contains bytes to send to a PHP-script // Step 2: encrypt request data and send encrypted data to a PHP-script // Generate a new random key SessionKeyRAW := TwofishInitSessionKeyRAW; SessionKey := TwofishInitKey(SessionKeyRAW); // Step 2a: encrypt the session key // Load public key from a file RSAKey := RSALoadPublicKey('C:\Documents\public.pem', rsPEM); try // Encrypt SessionKeyRAW into EncryptedData buffer EncryptedData.cbData := SizeOf(SessionKeyRAW); RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData); try // No longer needed SecureFree(RSAKey); SecureFree(SessionKeyRAW); // Convert bytes to text EncodedKey := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData); // No longer needed SecureFree(EncryptedData); finally // На всякий случай (исключение) - чистим данные SecureFree(EncryptedData); end; finally // Just in case (e.g. exception) - wipe everything SecureFree(RSAKey); SecureFree(SessionKeyRAW); end; // Now: // - EncodedKey stores encrypted session key (as a text) // - SessionKey stores session key (ready to use) // - JSONRAW stores request's bytes // Step 2b: encrypt the request // Since we do not use initialization vector - encryption will use ECB mode EncryptedData.cbData := Length(JSONRAW); TwofishEncrypt(SessionKey, Pointer(JSONRAW), Pointer(EncryptedData.pbData), EncryptedData.cbData); try // No longer needed SecureFree(SessionKey); // Convert bytes to text EncodedData := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData); // No longer needed SecureFree(EncryptedData); // Send both encrypted data and session key to the PHP-script // URLEncode is required to escape '+' in Base-64 URL := 'http://localhost/test.php?key=' + URLEncode(EncodedKey) + '&data=' + URLEncode(EncodedData); if not InitWebTools then RaiseLastOSError; try ReplyRAW := InternetGet(URL, [], []); finally DoneWebTools; end; // PHP-script does not return anything? // (should not happen normally) if ReplyRAW = '' then Abort; // Optional SecureFree(URL); // No longer needed SecureFree(EncodedKey); SecureFree(EncodedData); finally // Just in case (e.g. exception) - wipe everything SecureFree(SessionKey); SecureFree(EncryptedData); SecureFree(EncodedKey); SecureFree(EncodedData); end; finally // Just in case (e.g. exception) - wipe everything SecureFree(SessionKey); SecureFree(SessionKeyRAW); SecureFree(EncodedData); SecureFree(EncodedKey); SecureFree(JSONText); SecureFree(JSONRAW); end; // Now we have ReplyRAW - bytes from the PHP-script try // Convert bytes to text JSONText := UTF8ToString(ReplyRAW); // Optional Finalize(ReplyRAW); // Convert JSON-text to JSON-object JSON := JSONCreate(JSONText); // Optional Finalize(JSONText); // PHP-script has returned some error? if JSON.IndexOf('error') >= 0 then raise Exception.Create(JSON['error']); // Extract license and digital signature (text) EncodedLicense := JSON['license']; EncodedSignature := JSON['signature']; // Convert text to bytes License := Base64DecodeString(EncodedLicense); Signature := Base64DecodeString(EncodedSignature); // Load public key from a file RSAKey := RSALoadPublicKey('C:\Documents\public.pem', rsPEM); try // Verify the digital signature if RSAVerify(RSAKey, Pointer(License), Length(License), Pointer(Signature), Length(Signature)) then begin // Digital signature is not broken // This means that License really came from our server // Optional SecureFree(RSAKey); // Just as an example // Real application would probably have License encrypted ShowMessage(UTF8ToString(License)); // Will show 'This is just an example license' end else ShowMessage('Not signed'); finally // Just in case (e.g. exception) - wipe everything SecureFree(RSAKey); end; finally // Just in case (e.g. exception) - wipe everything SecureFree(JSONText); SecureFree(ReplyRAW); SecureFree(EncodedSignature); SecureFree(EncodedLicense); SecureFree(Signature); SecureFree(License); end; end;
<?php // These functions are required, because MCrypt uses zero padding instead of PKCS#5 // OpenSSL supports PKCS#5, but does not support Twofish // PKCS#5 padding function pkcs5_pad($text, $blocksize = 16) { $pad = $blocksize - (strlen($text) % $blocksize); return $text . str_repeat(chr($pad), $pad); } // Trims PKCS#5 padding function pkcs5_unpad($text) { $pad = ord($text{strlen($text)-1}); if ($pad > strlen($text)) { return false; } if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) { return false; } return substr($text, 0, -1 * $pad); } // Private key // Must be related to the public key, which was used in Delphi // If you are going to use this example, you need to replace this contant // This is just an example. Real program would store key in a file $PrivateKey = <<<EOD -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDkIEGZvxCUNW3u ... X+VZcAA+CUNp1xYUuep0UoeqYtfzTuKvinNo3PWAItyqtCJNcdNWDzbWRizMEPJK N9AYlDckmF/X/1cZxWNyiltZZ542kezn -----END PRIVATE KEY----- EOD; // Read arguments (encrypted session key and encrypted data) $EncodedKey = $_GET['key']; $EncodedData = $_GET['data']; // Convert text to bytes $EncryptedKey = base64_decode($EncodedKey); $EncryptedData = base64_decode($EncodedData); // Convert little-endian (Windows/EurekaLog/WinCrypt) to big-endian (PHP/OpenSSL) $EncryptedKey = strrev($EncryptedKey); // Decrypt the session key if (!openssl_private_decrypt($EncryptedKey, $Key, $PrivateKey)) { echo('{ "error": ' . json_encode('openssl_private_decrypt: ' . openssl_error_string()) . ' }'); die(); } // Decrypt the request $Data = pkcs5_unpad(mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedData, MCRYPT_MODE_ECB)); // Convert JSON-text to JSON-object $Data = json_decode($Data, true); // Simple checks to validate the request (just an example) $Request = $Data['request']; if (($Request['version'] < 1) || ($Request['version'] > 1)) { echo('{ "error": "Unsupported request" }'); die(); } // Are we being asked for a license? if ($Request['type'] == 'license') { // Validate the requester $User = $Data['user']; // Just an example $OK = (($User['login'] == 'input-from-edit1') && ($User['password'] == 'input-from-edit2')); if ($OK) { // Somehow obtain the license for the requester // Real app would probably send encrypted license $License = 'This is just an example license'; // Sign the license openssl_sign($License, $Signature, $PrivateKey, OPENSSL_ALGO_SHA1); // Convert big-endian (PHP/OpenSSL) to little-endian (Windows/EurekaLog/WinCrypt) $Signature = strrev($Signature); // Convert bytes to text $EncodedLicense = base64_encode($License); $EncodedSignature = base64_encode($Signature); // Return license and digital signature to a caller echo('{ "license": ' . json_encode($EncodedLicense) . ', "signature": ' . json_encode($EncodedSignature) . ' }'); } else { echo('{ "error": "Access Denied" }'); die(); } } echo('{ "error": "Unsupported request" }');