I'm running Windows Server 2k8 (maybe that's half the problem?) Anyway, I'm getting different values out of different Blowfish modules in various languages. Is there one which can be relied upon as the standard?
For the following examples, assume the key is password and the plaintext 12345678.
a. The Online Encrypt Tool with Algorithm set to Blowfish mode ECB and Base64 Encode the output checked gives 2mADZkZR0VM=. I've been using this a my reference point, wise or otherwise.
b. The following Perl code uses Crypt::ECB and MIME::Base64
use MIME::Base64;
use Crypt::ECB;
$crypt = Crypt::ECB->new;
$crypt->padding(PADDING_NONE);
$crypt->cipher('Blowfish') || die $crypt->errstring;
$crypt->key('password');
$enc = $crypt->encrypt("12345678");
print encode_base64($enc);
This outputs 2mADZkZR0VM= with PADDING_NONE (which compares well with 'a.' above). However, when padding is set to PADDING_AUTO it outputs 2mADZkZR0VOZ5o+S6D3OZw== which is, in my mind at least, a bug as the plaintext is 8 characters long and in no need of padding.
c. If I use Crypt::Blowfish as below
#! c:\perl\bin
use Crypt::Blowfish;
use MIME::Base64;
my $key;
my $plaintext;
$key = "password";
$plaintext = "12345678";
my $cipher = new Crypt::Blowfish $key;
my $ciphertext = $cipher->encrypt($plaintext);
my $encoded = encode_base64( $ciphertext );
print $encoded;
then I get 2mADZkZR0VM= which matches 'a.' above. Trouble with this module though is that one has to segment things into 8 byte chunks for encoding; it has no chunker of its own.
d. If I use the source at http://linux.die.net/man/3/bf_ecb_encrypt (which I did for a recent PHP ext project) then I get the same answer as 'a.'. I'm inclined to trust this code the most as it's in use in SSLeay and OpenSSL.
e. The BlowfishEx.EXE in DI Management's Blowfish: a Visual Basic version with PKCS#5 padding gives 2mADZkZR0VOZ5o+S6D3OZw== which is the same as the Crypt::ECB results with PADDING_AUTO. With Padding set to None I get 2mADZkZR0VM= which matches 'a.'
I've partly answered my own question writing this: looks like I just have to modify DI Management's code for the VB6 project. And maybe suggest the same to the writer of Crypt::ECB.
But the question remains: is there a trustworthy blowfish reference platform?
Your problem doesn't seem to be with whether they implement the Blowfish algorithm correctly. In the "no padding" variant, they all seem to produce identical results.
That leaves a question of whether an 8-byte input should be padded at all. The answer to this is pretty simple: PKCS #7 requires that there is always at least one byte of padding added to the input. The reason for this is simple: on the receiving end, there needs to be an unambiguous way to remove the padding. In the case of PKCS#7, the value of the padding bytes is always the number of bytes of padding that needs to be stripped off by the recipient.
So, when you receive material that's been padded with PKCS7, you decrypt the block, then look at the last byte in the decrypted output, and remove that many bytes from the block. Without a block of padding on the end, the receiver would normally look at the last byte of actual data, and whatever value that byte happened to contain, strip off that many bytes (though it should probably tell you there's a problem if the number is larger than 8). In any case, to assure correct operation, there must always be at least one byte of padding. In your case, that means the input gets expanded out to 16 bytes instead of 8.
Related
I am currently working on translating an encryption algorithm from PHP to Typescript, to use in a very specific API that requires the posted data to be encrypted with the API key and Secret. Here is the provided example of how to correctly encrypt data in PHP for use with the API (the way of implementing the key and IV can't be changed):
$iv = substr(hash("SHA256", $this->ApiKey, true), 0, 16);
$key = md5($this->ApiSecret);
$output = openssl_encrypt($Data, "AES-256-CBC", $key, OPENSSL_RAW_DATA, $iv);
$completedEncryption = $this->base64Url_Encode($output);
return $completedEncryption;
In the above code, the only thing the base64Url_Encode function does is convert the binary data to a valid Base64URL string.
And now the code as I have implemented it inside Typescript:
import { createHash, createCipheriv } from 'node:crypto'
const secretIV = createHash('sha256').update(this.ApiKey).digest().subarray(0, 16)
// Generate key
/*
Because the OpenSSL function in PHP automatically pads the string with /null chars,
do the same inside NodeJS, so that CreateCipherIV can accept it as a 32-byte key,
instead of a 16-byte one.
*/
const md5 = createHash('md5').update(this.ApiSecret).digest()
const key = Buffer.alloc(32)
key.set(md5, 0)
// Create Cipher
const cipher = createCipheriv('aes-256-cbc', key, secretIV)
let encrypted = cipher.update(data, 'utf8', 'binary');
encrypted += cipher.final('binary');
// Return base64URL string
return Buffer.from(encrypted).toString('base64url');
The above Typescript code only does NOT give the same output as the PHP code given earlier. I have looked into the original OpenSSL code, made sure that the padding algorithms are matching (pcks5 and pcks7) and checked if every input Buffer had the same byte length as the input inside PHP. I am currently thinking if it is some kind of binary malform that is causing the data to change inside Javascript?
I hope some expert can help me out with this question. Maybe I have overlooked something. Thanks in advance.
The stupidity is in the md5 function in PHP, which defaults to hexadecimal output instead of binary output:
md5(string $string, bool $binary = false): string
This is also why the code doesn't complain about the key (constructed from the MD5 hash) is being too small, it is fed 32 bytes after ASCII or UTF8 encoding, instead of the 16 bytes you'd use for AES-128.
Apparently it is using lowercase encoding, although not even that has been specified. You can indicate the encoding for NodeJS as well, see the documentation of the digest method. It also seems to be using lowercase, although I cannot directly find the exact specification of the encoding either.
Once you have completed your assignment, please try and remove the code ASAP, as you should never calculate the IV from the key; they key and IV combination should be unique, so the above code is not IND-CPA secure if the key is reused.
In case you are wondering why it is so stupid: the output of MD5 has been specified in standards, and is binary. Furthermore, it is impossible from the function to see what it is doing, you have to lookup the code. It will also work very badly if you're doing a compare; even if you are comparing strings then it is easy to use upper instead of lowercase (and both are equally valid, uppercase hex is actually easier to read for humans as we focus on the top part of letters more for some reason or other).
Basically it takes the principle of least surprise and tosses it out of the window. The encoding of the output could be made optimal instead, the NodeJS implementation does this correctly.
I am currently working on a new PHP site for a site currently utilizing ColdFusion 10. When the new site is ready the ColdFusion site will be decommissioned and I won't have access to anything related to ColdFusion. I don't want to have to reset all the previous passwords so need to be able to duplicate the one-way SHA-512 hash that is utilized in ColdFusion in PHP.
This question on Stack Overflow is very relevant to this problem:
hash function that works identically on ColdFusion MX7 and PHP 5.x?
The difference is they manually looped in 1024 iterations in ColdFusion. The ColdFusion site I am translating uses the built in iterations feature. I have tried what they did in the above question, plus a few variations including XOR in the end but ultimately I can't find documentation on what ColdFusion is doing during those iterations.
ColdFusion:
<cfset hpswd = Hash(FORM.npswd & salt, "SHA-512", "UTF-8", 1000) >
PHP (without iterations logic):
$hpswd = strtoupper(hash("sha512", $npswd.$salt));
Given this password: q7+Z6Wp#&#hQ
With this salt: F4DD573A-EC09-0A78-61B5DA6CBDB39F36
ColdFusion gives this Hash (with 1000 iterations): 1FA341B135918B61CB165AA67B33D024CC8243C679F20967A690C159D1A48FACFA4C57C33DDDE3D64539BF4211C44C8D1B18C787917CD779B2777856438E4D21
Even with making sure to strtoupper with PHP I have not managed to duplicate the iterations step so the question, what operands is ColdFusion 10+ doing during the iterations step?
Regardless of language, a SHA-512 hashing function should return the same output given the same inputs. Here, it looks like you may just need to ensure that your inputs are the same. This includes the encoding of the text you are inputting. Then you'll hash over it the same total number of times.
As of today, the CFDocs documentation of ColdFusion hash() is incorrect, but I have submitted a correction for that. See my comments above about why I believe Adobe lists their defaults this way. A Default of 1 Iteration is correct for Lucee CFML, but not for Adobe CF. You are correct that the ACF default is 0. CF2018 clarifies this parameter.
Now, to your issue, your original code in ACF10 is:
<cfset hpswd = Hash(FORM.npswd & salt, "SHA-512", "UTF-8", 1000) >
This says that you are hashing with the SHA-512 algorithm, using UTF-8 encoding, and repeating an additional 1000 times. This means that your hash() function is actually being called 1001 times for your final output.
So:
<cfset npswd="q7+Z6Wp#&##hQ">
<cfset salt = "F4DD573A-EC09-0A78-61B5DA6CBDB39F36">
<cfset hpswd = Hash(npswd & salt, "SHA-512","UTF-8",1000) >
<cfoutput>#hpswd#</cfoutput>
Gives us 1FA341B135918B61CB165AA67B33D024CC8243C679F20967A690C159D1A48FACFA4C57C33DDDE3D64539BF4211C44C8D1B18C787917CD779B2777856438E4D21.
https://trycf.com/gist/7212b3ee118664c5a7f1fb744b30212d/acf?theme=monokai
One thing to note is that the ColdFusion hash() function returns a HEXIDECIMAL string of the hashed input, but when it uses it's iteration argument, it iterates over the BINARY output of the hashed value. This will make a big difference in the final output.
https://trycf.com/gist/c879e9e900e8fd0aa23e766bc308e072/acf?theme=monokai
To do this in PHP, we'd do something like this:
NOTE: I am not a PHP developer, so this is probably not the best way to do this. Don't judge me, please. :-)
<?php
mb_internal_encoding("UTF-8");
$npswd="q7+Z6Wp#&#hQ";
$salt = "F4DD573A-EC09-0A78-61B5DA6CBDB39F36";
$hpswd = $npswd.$salt ;
for($i=1; $i<=1001; $i++){
$hpswd = hash("SHA512",$hpswd,true); // raw_output=true argument >> raw binary data.
// > https://www.php.net/manual/en/function.hash.php
}
echo(strtoupper(bin2hex($hpswd)));
?>
The first thing I do is ensure that the encoding we are using is UTF-8. Then I iterate over the given input string 1+1000 times. Using the raw_output argument of PHP hash() gives us binary representations each loop, which will give us the same final output. Afterwards, we use bin2hex() to convert the final binary value to a hexidecimal value, and then strtoupper() to uppercase it. Giving us 1FA341B135918B61CB165AA67B33D024CC8243C679F20967A690C159D1A48FACFA4C57C33DDDE3D64539BF4211C44C8D1B18C787917CD779B2777856438E4D21, matching the CF-hashed value.
Also note that CF returns an uppercase value whereas PHP is lowercase.
And final note: There are better methods for storing and using hashed passwords in PHP. This will help convert between CF and PHP hashes, but it would probably be better to ultimately convert all stored hashes into the PHP equivalents. https://www.php.net/manual/en/faq.passwords.php
=============================================================
A point of clarification:
Both Adobe and Lucee changed the name of this parameter to clarify their intent, however they behave differently.
Lucee named the parameter numIterations with default 1. This is the total times that hash() will run.
In CF2018, with the introduction of Named Parameters, Adobe renamed the parameter additionalIterations from the original (and still documented) iterations. The original improper parameter name didn't matter prior to CF2018 because you couldn't use named params anyway. On their hash() documentation page, their verbiage is "Hence, this parameter is the number of iterations + 1. The default number of additional iterations is 0." (emphasis mine) The behavior has always (since CF10) matched this description, but there is clearly some confusion about its actual meaning, especially since there is a difference with Lucee's behavior and with Adobe's incorrect initial name of the parameter.
The parameter name iterations is incorrect and doesn't work with either Adobe CF 2018 or Lucee 4.5 or 5.x. And this is a function that is not currently compatible as-is between Lucee and Adobe ColdFusion.
The important thing to remember, especially if working with both Adobe and Lucee code, is that this function with the ???Iterations named param specified will produce two different outputs if the same-ish code is run. Adobe will run a hash() one additional time vs Lucee. The good news is that since the param names aren't the same, then if they are used, an error will be thrown instead of silently producing different hashes.
hash("Stack Overflow","md5","UTF-8",42) ;
// Lucee: C0F20A4219490E4BF9F03ED51A546F27
// Adobe: 42C57ECBF9FF2B4BEC61010B7807165A
hash(input="Stack Overflow", algorithm="MD5", encoding="UTF-8", numIterations=42) ;
// Lucee: C0F20A4219490E4BF9F03ED51A546F27
// Adobe: Error: Parameter validation error
hash(string="Stack Overflow", algorithm="MD5", encoding="UTF-8", additionalIterations=42) ;
// Lucee: Error: argument [ADDITIONALITERATIONS] is not allowed
// Adobe: 42C57ECBF9FF2B4BEC61010B7807165A
https://helpx.adobe.com/coldfusion/cfml-reference/coldfusion-functions/functions-h-im/hash.html
https://docs.lucee.org/reference/functions/hash.html
See https://cfdocs.org/hash "...in Adobe CF the value is the number of additional iterations."
I'm encrypting the string "aaaaaaaaaaaaaaaa" (a 16 byte UTF8 string) using AES 128 CBC with a blank iv and key (16 0's) and getting different results
in PHP:
echo base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128,pack("H*", "00000000000000000000000000000000"),"aaaaaaaaaaaaaaaa",MCRYPT_MODE_CBC,pack("H*", "00000000000000000000000000000000")))
returns "kmwP6gWv1l9ZMdKanGs/nA=="
in Node.js:
let cipher = require('crypto').createCipheriv('aes-128-cbc',Buffer.alloc(16),Buffer.alloc(16))
console.log(cipher.update('aaaaaaaaaaaaaaaa','utf8','base64') + cipher.final('base64'))
returns "kmwP6gWv1l9ZMdKanGs/nHeUidae8Z4dK0HU7p2z+1c="
the first bit (bolded) is identical to PHP, but the PHP value has an extra 'A=' and then Node value has a whole extra 'HeUidae8Z4dK0HU7p2z+1c'
I'll admit that I'm pretty shaky on what's going on here - what am I missing here?
edit ... but not so shaky that I don't understand that what I'm doing here isn't particularly secure. Don't worry about whether this is the 'right' way to do encryption - please focus on the fact that the results ought to line up.
edit2 - I can, however, get close using hex rather than base64 -
PHP: 926c0fea05afd65f5931d29a9c6b3f9c
Node: 926c0fea05afd65f5931d29a9c6b3f9c779489d69ef19e1d2b41d4ee9db3fb57
the second chunk of Node hex is returned by the .final method, and I don't understand what it's for.
AES works on a block size of 16 bytes. You are encrypting the string "hello world", which is 11 bytes long in UTF8. Thus, padding is used to increase the length of your string to 16 bytes.
Node, and as it should, uses PKCS5 Padding to pad your plain-text to 16 bytes and then encrypts it.
mcrypt, as it shouldn't, uses zero bytes to pad your plain-text. mcrypt is deprecated and has been abandoned for a decade.
Because your two padding schemes are different, the plain-text is actually different before we even get to the point where we apply AES.
My advice: use the openssl_* functions in PHP instead. And don't use a static IV. Using a static IV makes your program vulnerable to some of the same vulnerabilities as ECB mode, which is not good!
According to the codeigniter documentation, the encryption key for an AES-128 MUST be set to 128bits/16bytes (16 characters) random string. I tested my output using var_dump function
(assuming I have set a 16 character key to the config file and already loaded the library)
My code:
$plain_text = 'Hello World';
$encrypted = $this->encryption->encrypt($plain_text);
var_dump($encrypted);
var_dump($this->encryption->decrypt($encrypted));
die();
The output
string(176) (A 176 character encrypted data appears)
string(11) Hello World
Sorry I'm not able to put the exact encrypted data but I believe is doesn't make sense to show it.
I would like to know if the encryption output is perfectly normal output considering its size(176 bytes)
AES is a block cipher so padding is to be expected.
A block cipher (as opposed to a stream cipher) can only operate on a pre-determined size and must pad a smaller plaintext in order reach that size before it can operate on it.
With that said, 176 bytes (11 x 16-byte blocks) is a lot bigger than 11 bytes and my guess is that part of the overhead may be because codeigniter encodes its own information (i.e. structured data such as type of variable, length of string, etc.) before encrypting it.
Since you are able to decrypt the ciphertext using the same key, it's probably not a bug in the implementation and is working as intended, though I can't speak for whether it was implemented properly and securely.
For quite sometime I've been trying to decipher the ASP .ASPXAUTH cookie and decrypt it using PHP. My reasons are huge and I need to do this, there is no alternative. In PHP so far I have successfully managed to read the data from this cookie, but I cannot seem to do it while it is encrypted. Anyway, here it goes...
First you need to alter your servers Web.config file (protection needs to be set to Validation):
<authentication mode="None">
<forms name=".ASPXAUTH" protection="Validation" cookieless="UseCookies" timeout="10080" enableCrossAppRedirects="true"/>
</authentication>
Then in a PHP script on the same domain, you can do the following to read the data, this is a very basic example, but is proof:
$authCookie = $_COOKIE['_ASPXAUTH'];
echo 'ASPXAUTH: '.$authCookie.'<br />'."\n";//This outputs your plaintext hex cookie
$packed = pack("H*",$authCookie);
$packed_exp = explode("\0",$packed);//This will separate your data using NULL
$random_bytes = array_shift($packed_exp);//This will shift off the random bytes
echo print_r($packed_exp,TRUE); //This will return your cookies data without the random bytes
This breaks down the cookie, or at least the unencrypted data:
Now that I know I can get the data, I removed the 'protection="validation"' string from my Web.config and I tried to decrypt it using PHP mcrypt. I have tried countless methods, but here is a promising example (which fails)...
define('ASP_DECRYPT_KEY','0BC95D748C57F6162519C165E0C5DEB69EA1145676F453AB93DA9645B067DFB8');//This is a decryption key found in my Machine.config file (please note this is forged for example)
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_RAND);
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, ASP_DECRYPT_KEY, $authCookie, MCRYPT_MODE_CBC, $iv);//$authCookie is the pack()'d cookie data
This however fails. I've tried variations of IV with all zeros # 16 bytes. I've tried different Rijndael sizes (128 vs 256). I've tried base64_decode()ing, nothing seems to work. I've found this stackoverflow post here and started using variations of the key/iv that are made using sha256, but that isn't really working either.
Anybody have a clue what I should do?
I don't know how encryption is made in .NET AuthCookies, but I can try to answer.
Assuming the encryption occurs in AES CBC-IV mode, with randomly generated IVs, you need to first find out where the IV is.
The code snippet you show cannot work, as you are generating a random IV (which will be incorrect). That being said, even if you get the IV wrong, in CBC mode you will only have the first 16 bytes of your decrypted ciphertext "garbled" and the rest will decrypt properly - you can use this as a test to know if you're doing the rest correctly. In practice when using random IVs, it's very likely that it's prepended to the ciphertext. To check if this correct, you can try to check if len(ciphertext) = len(plaintext) + 16. This would mean that most likely the first 16 bytes are your IV (and therefore it should be removed from the ciphertext before attempting to decrypt it).
Also on your code snippet, it seems you are using the key as an ascii-string, whereas it should be a byte array. Try:
define('ASP_DECRYPT_KEY',hex2bin('0BC95D748C57F6162519C165E0C5DEB69EA1145676F453AB93DA9645B067DFB8'));
Also, this seems to be a 32 byte key, so you need to use AES-256. I don't know how the authcookie looks like, but if it's base64 encoded, you also need to decode it first obviously.
Hope this helps!
Note: I don't recomment doing this for important production code, however - because there are many things that can go wrong if you try to implement even your own decryption routine as you are doing here. In particular, I would guess there should be a MAC tag somewhere that you have to check before attempting decryption, but there are many other things that can go wrong implementing your own crypto.
I understand this may not have been possible for the OP but for other people heading down this route here is a simple alternative.
Create a .net web service with a method like:
public FormsAuthenticationTicket DecryptFormsAuthCookie(string ticket)
{
return FormsAuthentication.Decrypt(ticket);
}
Pass cookie to web service from PHP:
$authCookie = $_COOKIE['.ASPXAUTH'];
$soapClient = new SoapClient("http://localhost/Service1.svc?wsdl");
$params= array(
"ticket" => $authCookie
);
$result = $soapClient->DecryptFormsAuthCookie($params);
I know what a pain is to decrypt in PHP something encrypted in .NET and vice versa.
I had to end up coding myself the Rijndael algorithm ( translated it from another language ).
Here is the link to the source code of the algorithm: http://pastebin.com/EnCJBLSY
At the end of the source code there is some usage example.
But on .NET, you should use zero padding when encrypting. Also test it with ECB mode, I'm not sure if CBC works.
Good luck and hope it helps
edit: the algorithm returns the hexadecimal string when encrypts, and also expects hexadecimal string when decrypting.