SHA 1 implementation in PHP and .Net - php

I'm working on a project(PHP Based) in which I need to compute SHA1, I'm using this line of code to generate SHA1 in PHP.
$da = file_get_contents("payload.txt");
echo sha1($da);
and this is the code for the .Net
private static string GetSHA1(string text)
{
UnicodeEncoding UE = new UnicodeEncoding();
byte[] hashValue;
byte[] message = UE.GetBytes(text);
SHA1Managed hashString = new SHA1Managed();
string hex = "";
hashValue = hashString.ComputeHash(message);
foreach (byte x in hashValue)
{
hex += String.Format("{0:x2}", x);
}
return hex;
}
But I'm confused because both of languages generate different results, I'm not provide any salt in both of them(as I don't know what to use in salt b/c the api didn't defined that)
I also need to work on RSA after that(I've a key for the encryption)
Also Can anyone tell, does these algorithms differ due to languages or any thing I'm missing???
Need some experts opinion on this
this is the whole Algorithm to generate SHA1 and RSA encryption
<?php
class MyEncryption
{
public $pubkey = '...public key here...';
public $privkey = '...private key here...';
public function encrypt($data)
{
if (openssl_private_encrypt($data, $encrypted, $this->pubkey))
$data = base64_encode($encrypted);
else
throw new Exception('Unable to encrypt data. Perhaps it is bigger than the key size?');
return $data;
}
public function decrypt($data)
{
if (openssl_private_decrypt(base64_decode($data), $decrypted, $this->privkey))
$data = $decrypted;
else
$data = '';
return $data;
}
}
$enc = new MyEncryption();
$enc->pubkey = file_get_contents("server.key");
$payload = file_get_contents("payload1.txt");//payload1.txt contain xml data
$hashRes = sha1($payload,true);
echo $enc->encrypt($hashRes);
?>
Thanks alot

It's your UnicodeEncoding. PHP doesn't know anything about encodings, so your string simply contains all the bytes that the file does, 1:1. In .NET however, if you read the file as text, it will assume some encoding (typically UTF-8, if you don't specify otherwise) and then converts it to the internal representation, and in the end you convert it to UTF-16 and hash those bytes - which are probably nothing like the original at bytes all, unless the original also was UTF-16. And even if it was UTF-16, it might have included the Byte-Order-Mark, which UnicodeEncoding.GetBytes() doesn't. And even if it didn't, the original string might not have been normalized, and you might have normalized it somewhere along the way (you don't show us the code where you read it), and the bytes will be different again. Etc.
The correct approach would be to read the whole file as a binary block of bytes (for example, with System.IO.File.ReadAllBytes()), and then hash those bytes directly. There's no need to involve any text encodings and conversions. And then the hashes should match.

Have you tried sha1(data, true)? Otherwise it will generate hexadecimals. You can also use hex2bin(sha1(data)) if it is not available on your platform. Also, UnicodeEncoding may use 2 bytes as encoding instead of one (for characters that are also in ASCII). Try UTF8Encoding instead.

Related

Nodejs how to implement OpenSSL AES-CBC encryption (from PHP)?

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.

Convert .net aes256 encryption code into php

The code below is a simple .NET snippet, having test on the input it returns p+cTm2VODfvQnreAl02wUQ== as an output.
Dim aesEncryptObj As New System.Security.Cryptography.RijndaelManaged()
Dim encoder As System.Text.ASCIIEncoding = New System.Text.ASCIIEncoding()
Dim tempKey As Byte() = encoder.GetBytes("00000011111111111111111111111111")
aesEncryptObj.Key = tempKey
aesEncryptObj.BlockSize = 128
aesEncryptObj.Mode = System.Security.Cryptography.CipherMode.ECB
aesEncryptObj.Padding = System.Security.Cryptography.PaddingMode.PKCS7
aesEncryptObj.GenerateIV()
Dim EncryptedBytes As Byte()
Dim encryptor As System.Security.Cryptography.ICryptoTransform = aesEncryptObj.CreateEncryptor(aesEncryptObj.Key, aesEncryptObj.IV)
Using msEncrypt As New System.IO.MemoryStream()
Using csEncrypt As New System.Security.Cryptography.CryptoStream(msEncrypt, encryptor, System.Security.Cryptography.CryptoStreamMode.Write)
Using swEncrypt As New System.IO.StreamWriter(csEncrypt)
swEncrypt.Write(txtInput.Text)
End Using
EncryptedBytes = msEncrypt.ToArray()
End Using
End Using
txtOutput.Text = Convert.ToBase64String(EncryptedBytes)
Now, here is the PHP code:
const ENCRYPT_METHOD = 'aes-256-ecb';
$aesKey = pack('H*', '00000011111111111111111111111111');
$ivSize = openssl_cipher_iv_length(ENCRYPT_METHOD);
$plainText = "test";
$iv = openssl_random_pseudo_bytes($ivSize);
$cipherText = openssl_encrypt(
$plainText,
ENCRYPT_METHOD,
$aesKey,
OPENSSL_RAW_DATA,
$iv
);
$encryptedText = $iv . $cipherText;
echo base64_encode($encryptedText);
It returns 1W3UvYVNKWEoFrpPZPd+Qw== which differs from the .NET one. I've tried both aes-256-ecb and aes-128-ecb and the result is always different from the .NET's one.
As far as I know, openssl_encrypt do the PKCS7 padding by default, is that right? Can you see the reason why PHP is giving different result?
Your code isn't working because:
GetBytes in .NET returns you the byte values of that string. That is, you are going to get a byte array with a length of 32 (AES-256). However, pack in PHP with H* decodes a hex string, which is going to give you a key of length 16 (AES-128). This whole time you've been encrypting with not only two different keys, but two different key sizes.
Fixing the above will make your code work, but it will be far from actually secure and should not be used. If you want to make your code secure, you need to:
Stop using ECB mode. Use GCM mode if you can, CBC otherwise. You are most likely going to end up using CBC. If you do, you need to prepend the IV to your ciphertext so that the decrypting party can retrieve it and use it when decrypting. IVs don't need to be secret, they just need to be random, so your use of OpenSSL random bytes and the GenerateIV method is good.
Since you are likely going to be using CBC mode, you will need to apply an HMAC to ensure integrity. Otherwise anyone can modify your ciphertext and you won't know. When applying an HMAC, always encrypt-then-mac. Remember to include your IV before applying the HMAC.
Lastly, and I don't think you actually meant to do this, but never use the ASCII values of a string as a key. If you want to create a key from a passphrase, you should apply a KDF. PBKDF2 is probably the best suited and has implementations in both PHP and .NET.

Avoid SHA1 hashing in openssl_sign / sign given hash

I'm working on replacing a legacy system that (among other things) receives SHA1 hashes of arbitrary files and signs them using a private key with a simple PHP web service.
It should look something like that:
$providedInput = '13A0227580C5DE137C2EBB2907A3F2D7F00CA71D';
// pseudo "= sha1(somefile.txt); file not available server side!
$expectedOutput = 'DBC9CC4CB0BECEE313BB100DD1AD39AEC045714D72767211FD574E3E3546EB55E77D2EBFE33BA2974BB74CE051608BFF45A73A52612C5FC418DD3A76CAC0AE0C8FB3FC6CE4F7A516013A9743A36424DDACFE889B3D45E86E6853FD9A55B5B4F0F0D8A574A0B244C0946A99B81CCBD1A7AF7C11072745B11C06AD680BE8AC4CB4';
// pseudo: "= openssl_sign(file_get_contents(somefile.txt), signature, privateKeID);
For the sake of simplicity I'm using PHP's built in openssl extention. The problem I'm running into is that openssl_sign seems to SHA1 hash the input data again internally according to this German manual entry on openssl_sign. The English entry is missing that info for some reason.
This produces the expected output ...
$privateKeyID = openssl_get_privatekey(file_get_contents($privateKey));
openssl_sign(file_get_contents("x.txt"), $signature, $privateKeyID);
var_dump(bin2hex($signature));
... but since I don't have access to the actual input files on server side it's not very helpful.
Is there a way around the additional hashing without 3rd party libs? I already tried to simply encrypt the hash received, but from How to compute RSA-SHA1(sha1WithRSAEncryption) value I understand encrypting and signing produce different output.
Update to make things more clear:
I'm recieving an SHA1 hash as input and the service has to convert it to a valid signature (using a private key) that can simply be verified using openssl_verify. The clients are out of reach, so changing their implementation is not possible.
From How to compute RSA-SHA1(sha1WithRSAEncryption) value:
If you reproduce this EM and use RSA_private_encrypt, then you will get the correct PKCS#1 v1.5 signature encoding, the same you would get with RSA_sign or even better, using the generic EVP_PKEY_sign.
I figured I could simply implement the DER encoding myself according to this specification, but the result (EM) seems too long to be encrypted with my key
// 1. Apply the hash function to the message M to produce a hash value H
$H = hex2bin($input); // web service receives sha1 hash of an arbitrary file as input
$emLen = 128; // 1024 rsa key
// 2. Encode the algorithm ID for the hash function and the hash value into
// an ASN.1 value of type DigestInfo
$algorithmIdentifier = pack('H*', '3021300906052b0e03021a05000414');
$digest = $H;
$digestInfo = $algorithmIdentifier.$digest;
$tLen = strlen($digestInfo);
// 3. error checks omitted ...
// 4. Generate an octet string PS consisting of emLen - tLen - 3 octets
// with hexadecimal value 0xff. The length of PS will be at least 8
// octets.
$ps = str_repeat(chr(0xFF), $emLen - $tLen - 3);
//5. Concatenate PS, the DER encoding T, and other padding to form the
// encoded message EM as
$em = "\0\1$ps\0$digestInfo";
if(!openssl_private_encrypt($em, $signature, $privateKeyID)) {
echo openssl_error_string();
}
else {
echo bin2hex($signature);
}
Output:
Error:0406C06E:rsa routines:RSA_padding_add_PKCS1_type_1:data too large for key size
Any hints?
UPDATE
As you can see in code below openssl_verify return 1 for result of openssl_sign and even for openssl_private_encrypt result. I tested it on my machine. This solution will work only if sha1 digest in digital signature is used.
// Content of file
$data = 'content of file somewhere far away';
// SHA1 hash from file - input data
$digest = hash('sha1', $data);
// private and public keys used for signing
$private_key = openssl_pkey_get_private('file://mykey.pem');
$public_key = openssl_pkey_get_public('file://mykey.pub');
// Encoded ASN1 structure for encryption
$der = pack('H*', '3021300906052b0e03021a05000414') . pack('H*', $digest);
// Signature without openssl_sign()
openssl_private_encrypt($der, $signature, $private_key);
// Signature with openssl_sign (from original data)
openssl_sign($data, $opensslSignature, $private_key);
// Verifying - both should return 1
var_dump(openssl_verify($data, $signature, $public_key));
var_dump(openssl_verify($data, $opensslSignature, $public_key));
I just captured DER encoded structure by decrypting openssl_sign() result.
ORIGINAL ANSWER
openssl_sign() creates digest from data because this is how digital signature works. Digital signature is always encrypted digest from data.
You can use openssl_private_encrypt() and openssl_public_decrypt() on your sha1 digest with no fear. In general, it is the same thing but yes, there is a difference. If you encrypt something on your own, the encryption process does not care about data and just encrypts them. It is on you to know that what you will decrypt later is sha1 digest for some data. In fact, it is just data encryption with private key, not true digital signature.
openssl_sign() creates digest from data and encrypts information about kind of digest and digest itself (this is ASN.1 DER structure from your link). This is because openssl_verify() needs to know what kind of digest was used when signing.
According to the English page of openssl_sign:
bool openssl_sign ( string $data , string &$signature , mixed $priv_key_id [, mixed $signature_alg = OPENSSL_ALGO_SHA1 ] )
I think the obvious suggestion is to use OPENSSL_ALGO_SHA256. See openssl_get_md_methods for a list of the supported algorithms.

Is the raw output returned by PHP's hashing functions the same as pack('H*')?

PHP's various hashing functions like md5(), sha1(), hash(), hash_pbkdf2() and hash_hmac() allow you to set $raw_output = false and get raw binary data.
I have put together a class that encrypts using mcrypt and hash_pbkdf2. When creating the key both of the following code work:
// Version 1
private function createKey($salt, $password) {
// Hash the key
$key = hash_pbkdf2('sha256', $password, $salt, 5000, 32, true);
return $key;
}
// Version 2
private function createKey($salt, $password) {
// Hash the key
$key = hash_pbkdf2('sha256', $password, $salt, 5000);
// Pack the key into a binary hex string
$key = pack('H*', $key);
return $key;
}
They both produce the same key size but are they both doing the same thing? Is either more secure than the other given that I'm using this for cryptography?
If you want to have raw binary result, you should go with hash_pbkdf2 with $raw_output = true. It will generate the bytes that you want without any encoding.
hash_pbkdf2 internally uses bin2hex to then encode the binary string into hex. This is an additional operation. When you then use pack to decode the hex into the binary string, you have yet another unnecessary operation.
Using hex output and decoding it back to binary does not improve your security. If anything, it might decrease it, because password hackers might not use these unnecessary steps.
If you're concerned whether bin2hex actually reverses pack("H*"), you should use hex2bin as those are companion functions that are probably reversing each other.

Node.js: how to decipher text ciphered in php?

My PHP ciphering looks like this:
<?
$salt = '…';
$data = '…';
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB), MCRYPT_RAND);
$ciphered = trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $salt, $data, MCRYPT_MODE_ECB,$iv)));
I'm trying to decipher the result of the code above with:
ciphered = '…';
crypto = require('crypto');
salt = crypto.createHash('md5').update('…').digest('hex');
iv = '0123456789123455';
decipher = crypto.createDecipheriv('aes-256-cbc', salt, iv);
deciphered = decipher.update(ciphered, 'base64');
deciphered += decipher.final('utf-8');
This code results in: TypeError: DecipherFinal fail
a couple of problems I see:
mismatch of operating modes. you generate an IV for a CFB (Cipher Feedback) operating mode,you use ECB (Electronic Code Book - not recommended, just check out the image in that wiki article for why) as your mode when you actually encrypt, then try to decrypt using CBC (Cipher Block Chaining) mode. You should stick to one mode (probably CBC). To do this keep the decryption side aes-256-cbc and make the encryption side MCRYPT_MODE_CBC
you pass $salt (which is actually your key) into mcrypt_encrypt without hashing it, but do hash it, and return a hex string when crypto.createDecipheriv expects a binary encoded string, per its documentation. Both keys need to be the same, and need to follow the proper encoding so that they remain the same when passed into the functions.
It looks like you generate an IV on the encryption side, then use a fixed string for an IV on the decryption side. The IV (Initialization vector) needs to be communicated with the ciphertext to the decryption side (and it is okay to be transmitted in the clear along with the ciphertext).
the update method on your decipher object does not accept base64 as an encoding, per its documentation. You will need to convert your base64 text to something else (probably a binary encoding) and then pass it into the update method with the proper encoding.
PHP's default charset is ISO-8859-1, but you are trying to decrypt your ciphertext as a UTF-8 string. This may cause problems, especially if you use characters beyond those used in standard ASCII. you either need to make sure that your PHP side is operating in UTF-8 mode (check out this SO answer on how to do that), or ensure that your input only uses ASCII characters (ISO-8859-1 is a superset of ASCII) and use the 'ascii' output encoding.
Most of your problems boil down to encoding issues. I don't know a lot about the various types of encoding on node.js, so you will need to research that on your own, but the issues with the cryptographic primitives should be easy to fix. make sure to read the documentation I linked and the mcrypt_encrypt documentation as well.

Categories