I am trying to write some Perl code to decode Triple DES (3DES) keys generated by a PHP program. I've looked for an example of using Crypt::GCrypt decoding in Perl but can't find one.
I need a Perl equivalent to PHP's openssl_decode(), but even an encrypt/decrypt of a string does not match.
This is my test code. All values are samples for testing.
use strict;
{
# match PHP's openssl_encrypt($plain_text, 'des3', $pw, 0, $iv );
# openssl_decrypt($cipher_text,'des3', $pw, 0, $iv );
# $ee = Exxx->new( %args );
# $cipher_text = $ee->encode( $plain_text );
# $plain_text = $ee->decode( $cipher_text );
package Exxx;
use Crypt::GCrypt;
use HTML::Entities;
use MIME::Base64;
my $ex_ = 0;
my $exxx_method = $ex_ ++;
my $exxx_password = $ex_ ++;
my $exxx_iv = $ex_ ++;
my $exxx_cipher = $ex_ ++;
sub new {
my ( $class, $method, $passwd, $iv ) = #_;
my $type = ref($class) ? ref($class) : __PACKAGE__;
my $this = [];
$this->[$exxx_method] = $method ? $method : '3des';
$this->[$exxx_method] = '3des' # map any PHP name to a Perl name
if $this->[$exxx_method] eq 'des3';
$this->[$exxx_password] = pack(
'H*',
$passwd ? $passwd
: '0123456789ABCDEF' . # key 1
'FEDCBA9876543210' . # key 2
'3243F6A8885A308D' ); # key 3
$this->[$exxx_iv] = pack(
'H*',
$iv ? $iv
: '2b7e151628aed2a6' );
bless $this, $type;
return $this;
}
sub _cipher {
my ( $this, $encrypting_decrypting ) = #_;
my $cipher = Crypt::GCrypt->new(
type => 'cipher',
algorithm => $this->[$exxx_method],
mode => 'cbc',
padding => 'standard',
);
$cipher->start($encrypting_decrypting);
$cipher->setiv( $this->[$exxx_iv] );
return $cipher;
}
sub encrypt {
my ( $this, $plain_text ) = #_;
my $cipher = $this->_cipher("encrypting");
$cipher->encrypt($plain_text);
my $encrypted = $cipher->finish();
return encode_base64( $encrypted, "" );
}
sub encrypt_html {
my ( $this, $plain_html ) = #_;
return $this->encrypt( decode_entities($plain_html) );
}
sub decrypt {
my ( $this, $crypt_text ) = #_;
my $cipher = $this->_cipher("decrypting");
my $plain_text = $cipher->decrypt( decode_base64($crypt_text) );
my $f = $cipher->finish();
return $plain_text;
}
}
############################
my $exit = 0;
my $plain = 'Hello, world'; #clear text for both PHP and PERL
my $encrypted_php = 'c0WJDTwtcBsj1vTfTi7jwA=='; #crypted from PHP program
my $exxx = Exxx->new('des3');
my $encrypted = $exxx->encrypt($plain);
if ( $encrypted eq $encrypted_php ) {
print "PASS encrypt: (perl)$encrypted eq (php)$encrypted_php\n";
}
else { #trouble.... does not match PHP
print STDERR "ERROR encrypt: (perl)$encrypted ne (php)$encrypted_php\n";
$exit = 1;
}
my $decrypted = $exxx->decrypt($encrypted);
if ( $plain eq $decrypted ) { #trouble.... does not match PHP
print "PASS decrypt: (perl)$plain == (perl)$decrypted\n";
$exit = 1;
}
{ #trouble.... does not match PHP
print STDERR "ERROR decrypt: (perl)$plain ne (perl)$decrypted\n";
$exit = 1;
}
exit $exit;
I recommend that you use the excellent Crypt::Cipher library suite. It covers a huge number of ciphers, it is independent of any other module, and I have compiled it successfully using Visual C as well as gcc and g++ without any issues
You will need to specify DES_EDE (same as Triple DES and 3DES) when you create a new Crypt::Cipher object, and the rest should be obvious
Crypt::Cipher->new('DES_EDE', $key);
Related
I've tried to do user verification script for telegram web app for bots. I have no idea how to fix it. The hash and encoded string are similar but not the same.
Telegram article (documentation): https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app
$bot_token="5368097647:AAFU8jBho71pglrzDeGw6LawrVuMmxaxpEQ";
$data_check_string=$_POST['a'];
$data_check_string=explode("&", urldecode($data_check_string));
foreach ($data_check_string as &$arrvalue) {
$hash="";
if (substr($arrvalue, 0, 4)=='hash'){
$hash=explode("=", $arrvalue)[1];
break;
}
}
sort($data_check_string);
$data_check_string=implode('\\n', $data_check_string);
$secret_key = $sig = hash_hmac('sha256', $bot_token, "WebAppData");
if (hash_hmac('sha256', $data_check_string, $secret_key) == $hash) {
print("fromtg");
}
else{
print("notg");
}
?>
Join the array with implode("\n", $data_check_arr);
in the hash_mac functions set the fourth param to TRUE (outputs raw binary data);
before compare the two hash apply bin2hex function to calculated hash to convert binary data into hexadecimal representation;
Here my code:
<?php
$data_check_arr = explode('&', rawurldecode($data_check_string));
$needle = 'hash=';
$check_hash = FALSE;
foreach( $data_check_arr AS &$val ){
if( substr( $val, 0, strlen($needle) ) === $needle ){
$check_hash = substr_replace( $val, '', 0, strlen($needle) );
$val = NULL;
}
}
// if( $check_hash === FALSE ) return FALSE;
$data_check_arr = array_filter($data_check_arr);
sort($data_check_arr);
$data_check_string = implode("\n", $data_check_arr);
$secret_key = hash_hmac( 'sha256', $bot_token, "WebAppData", TRUE );
$hash = bin2hex( hash_hmac( 'sha256', $data_check_string, $secret_key, TRUE ) );
if( strcmp($hash, $check_hash) === 0 ){
// validation success
}else{
// validation failed
}
I am comparing two base64_encode md5 keys created with same values (IP,Time,Path & Password) on different domains.
Create Encrypted key on 1st domain
$secret = "PASSWORD";
$expires = time()+3600;
$uri = '/video1/';
$ip = $_SERVER['REMOTE_ADDR'];
$md5 = base64_encode(md5($secret . $expires . $uri . $ip, true));
$md5 = strtr($md5, '+/', '-_');
$md5 = str_replace('=', '', $md5);
$rtmp = "?md5=".$md5;
$urls= 'http://example.com'.$uri .$rtmp;
echo '' .$urls . '';
Create Encrypted key on 2nd domain and compare with URL received Key
function getAddress() {
$protocol = $_SERVER['HTTPS'] == 'on' ? 'https' : 'http';
return $protocol.'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
}
$url = getAddress();
$path = (parse_url($url, PHP_URL_PATH)); // recive path here '/video1/'
$verify = substr(parse_url($url, PHP_URL_QUERY),4); //recive md5 encoded key from URL
/* create again md5 encoded key to match with URL key */
$secret = "PASSWORD";
$expires = time()+3600;
$uri = $path;
$ip = $_SERVER['REMOTE_ADDR'];
$md5 = base64_encode(md5($secret . $expires . $uri . $ip, true));
$md5 = strtr($md5, '+/', '-_');
$md5 = str_replace('=', '', $md5);
$rtmp = $md5;
if ($rtmp===$verify){ // Matching both, local key with URL key
echo '<h1>Welcome</h1>';
}
else {
echo '<h1>Password,Time,Path or IP Not Match</h1>';
}
I used time(3600) in Encryption, So if statement should be show value for 3600 second. But this always show else value.
How this will be print if value for time(3600)? After that time print else
Perhaps above and beyond the scope of the question but the whole MD5 approach was flawed so why not go the "whole hog" ( as the saying goes ) and actually use encryption rather than hashing as in the question?
The encrypt and decrypt functions are based upon code found in the PHP manual for openssl_encrypt
function encrypt( $data=false, $pubkey=false, $cipher='AES-128-CBC' ){
if( !empty( $data ) && in_array( $cipher, openssl_get_cipher_methods() ) ){
$ivlen = openssl_cipher_iv_length( $cipher );
$iv = openssl_random_pseudo_bytes( $ivlen );
$encrypted = openssl_encrypt( $data, $cipher, $pubkey, $options=OPENSSL_RAW_DATA, $iv );
$hash = makehash( $encrypted, $pubkey );
return base64_encode( $iv . $hash . $encrypted );
}
return false;
}
function decrypt( $data, $pubkey=false, $cipher='AES-128-CBC' ){
if( !empty( $data ) && in_array( $cipher, openssl_get_cipher_methods() ) ){
$shalength=32;
$data = base64_decode( $data );
$ivlen = openssl_cipher_iv_length( $cipher );
$iv = substr( $data, 0, $ivlen );
$hash = substr( $data, $ivlen, $shalength );
$encrypted = substr( $data, $ivlen + $shalength );
$decrypted = openssl_decrypt( $encrypted, $cipher, $pubkey, $options=OPENSSL_RAW_DATA, $iv );
if( $decrypted && hash_equals( $hash, makehash( $encrypted, $pubkey ) ) ){
return $decrypted;
}
}
return false;
}
function makehash( $data, $key ){
return hash_hmac( 'sha256', $data, $key, true );
}
Then, to use it:
$lifetime=3600;
$key='A complex secret string - ideally this will be the contents of an ssl cert perhaps obtained using file_get_contents etc';
$cipher='AES-128-CBC';
/* Create the payload of items to be encrypted and passed in the url */
$payload=array(
'endpoint' => '/secret-forum/topic404',
'expires' => time() + $lifetime,
'ip' => $_SERVER['REMOTE_ADDR']
);
/* create a nice string to be encrypted */
$data=urldecode( http_build_query( $payload ) );
/* create the encrypted data string */
$encrypted=encrypt( $data, $key, $cipher );
/* construct the url to be presented to the user */
$url=sprintf( '%s://%s/?hash=%s', $_SERVER['REQUEST_SCHEME'], $_SERVER['HTTP_HOST'], $encrypted );
printf('%1$s', $url);
/* At the Server - To process the url and check validity */
$querystring = parse_url( $url, PHP_URL_QUERY );
if( !empty( $querystring ) ){
list( $param, $data )=explode( '=', $querystring );
/* decrypt data */
$decrypted=decrypt( $data, $key, $cipher );
if( $decrypted ){
/* process initial querystring we created - create an array $out */
parse_str( $decrypted, $out );
/* for simplicity, cast as an object to use object notation */
$obj=(object)$out;
$endpoint=$obj->endpoint;
$expires=$obj->expires;
$ip=$obj->ip;
/* perform logic tests on the decrypted data and act accordingly */
if( time() > $expires or $ip!=$_SERVER['REMOTE_ADDR'] ){
/* too late */
printf( '<h1>That link has now expired</h1><p>You are no longer premitted to access that resource</p>' );
} else {
/* all good */
printf( '<h1>Welcome</h1><p>%s</p>', $obj->endpoint );
}
}
}
I haven't copied all parts of your code but this is the principle.
Hash the password and time separate, that way you can make sure the password is correct and look at the time independently.
$secret = "PASSWORD";
$expires = time()+3600;
$urls= 'http://example.com?md5=" . md5($secret) . "&t=" . md5($expires);
This will pass them independently and on the receiving end you match the password with password and you loop the time to see if it's valid.
if($_GET['md5'] == $password) $validM = true;
for($i = time()+3600; $i>time(); $i--){
if(md5($i) == $_GET['t']) $validT = true;
}
if($validM && $validT){
echo "within 3600 seconds and correct password";
}
This is a safe method for about 95% of the world's population, but since we are passing password and time variables by GET, it's not that hard to figure out how to get illegal access.
If it's something that needs safe transactions you are using this for then don't use this method.
I'm trying to port over the following code to Node.js with no luck.
$output = false;
$key = hash( 'sha256', self::SECRET_KEY );
$iv = substr( hash( 'sha256', self::SECRET_IV ), 0, 16 );
if( $action == 'e' ) {
$output = base64_encode( openssl_encrypt( $string, self::ENCRYPT_METHOD, $key, 0, $iv ) );
}
else if( $action == 'd' ){
$output = openssl_decrypt( base64_decode( $string ), self::ENCRYPT_METHOD, $key, 0, $iv );
}
return $output;
This is where I am so far, but it's either spitting out gibberish or I get one of several errors.
var crypto = require('crypto'),
algorithm = ENCRYPT_METHOD,
password = SECRET_IV;
function encrypt(text){
var cipher = crypto.createCipher(algorithm, sha256(SECRET_KEY), sha256(SECRET_IV).substring(0,16))
let p = Buffer.from(text, 'utf8').toString('base64');
var crypted = cipher.update(p,'utf8','base64');
crypted += cipher.final('base64');
return crypted;
}
function sha256(data) {
return crypto.createHash("sha256").update(data).digest("hex");
}
function decrypt(text){
var decipher = crypto.createDecipher(algorithm, sha256(SECRET_KEY), sha256(SECRET_IV).substring(0,16));
decipher.setAutoPadding(false);
var dec = decipher.update(text,'base64','utf-8');
dec += decipher.final('utf8');
return dec;
}
I've tried decoding/encoding with base64, but this also does not work. Once I added setAutoPaddding to false, I started making some progress. I found on one thread that this is an issue when encoding/decoding from one language on Node.
Any help would be appreciated.
I have been trying to find out how to make this:
function encrypt_decrypt($action, $string) {
$output = false;
$encrypt_method = "AES-256-CBC";
$secret_key = 'HqFdkh2FX126fH1r';
$secret_iv = 'iS2dk82dXd26f61K';
// hash
$key = hash('sha256', $secret_key);
// iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning
$iv = substr(hash('sha256', $secret_iv), 0, 16);
if ( $action == 'encrypt' ) {
$output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
$output = base64_encode($output);
} else if( $action == 'decrypt' ) {
$output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
}
return $output;
}
In Node.js
The reason is that the encryption will be handled by PHP and the decryption by Node.
EDIT:
I managed to get this far:
var crypto = require('crypto')
, key = 'cI8Jd96NDoasd09jcI8Jd96NDoasd09j'
, iv = 'cI8Jd96NDoasd09j'
, plaintext = '2';
hashedKey = crypto.createHash('sha256').update(key, 'utf-8').digest('hex');
console.log('hashed key=', hashedKey);
// corresponds to the hashed key in PHP
hashedIv = crypto.createHash('sha256').update(iv, 'utf-8').digest('hex').substring(0,16);
console.log('hashed iv=', hashedIv);
// corresponds to the hashed iv in PHP
var buf = Buffer.from(teamId, 'base64');
console.log("buffer: " + buf);
and the variable buf actually is the same as base64_decode($string in the PHP code.
However, when I do this:
var decipher = crypto.createDecipheriv("aes-256-cbc",key, iv);
var decrypted = decipher.update(buf, 'base64', 'utf8');
console.log("decrypted.toString(): " + decrypted.toString());
I'm getting Z���ߋd�M:�� in the console rather than the desired 2.
The main problem was an embarrasing one. We are mainly two devs on this project and I thought the php-file I was editing for the encryption and decryption was the only thing I had to care about.
It was later realized that the actual call for the encoding was made from another php-file. Thus, what I changed in the encoding in the file I was working on was all in vain.
The end result looks like this, for anyone who's interested:
function encrypt_decrypt($action, $string) {
$output = false;
$encrypt_method = "AES-256-CBC";
$secret_key = '32 byte key';
$secret_iv = '16 byte iv';
// hash
$key = substr(hash('sha256', $secret_key), 0, 32);
// iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning
$iv = substr(hash('sha256', $secret_iv), 0, 16);
if ( $action == 'encrypt' ) {
$output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
//$output = base64_encode($output);
} else if( $action == 'decrypt' ) { // this below is now handled in Node
$output = openssl_decrypt($string, $encrypt_method, $key, 0, $iv);
//$output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
}
return $output;
}
And Node:
function getDecryptedTeamId(encryptedId) {
var hashedKey;
var hashedIv;
var crypto = require('crypto')
, key = 'same 32 byte key as above in php'
, iv = 'same 16 byte ivas above in php'
, plaintext = '2';
hashedKey = crypto.createHash('sha256').update(key, 'utf-8').digest('hex').substring(0,32);
key = hashedKey;
hashedIv = crypto.createHash('sha256').update(iv, 'utf-8').digest('hex').substring(0,16);
iv = hashedIv;
var buf = Buffer.from(encryptedId, 'base64');
var crypt = buf.toString('base64');
var decryptor = crypto.createDecipheriv("aes-256-cbc", hashedKey, hashedIv);
var teamIdDec = decryptor.update(buf);
teamIdDec += decryptor.final();
return teamIdDec;
}
I am using the following function for encrypt and decrypt of wordpress post content in a WordPress plugin:
public static function afz_encrypt_decrypt( $action, $string ) {
$output = false;
$encrypt_method = "AES-256-CBC";
$secret_key = '12345678901234567890123456789012';
$iv_size = 16;
if ( $action == 'encrypt' ) {
$encrypt_iv = mcrypt_create_iv( $iv_size, MCRYPT_DEV_URANDOM );
$output = openssl_encrypt( $string, $encrypt_method, $secret_key, 0, $encrypt_iv );
$output = base64_encode( $encrypt_iv . $output );
} else if ( $action == 'decrypt' ) {
$decrypt_iv = substr( base64_decode( $string ), 0, $iv_size );
$output = stripslashes( openssl_decrypt( substr( base64_decode( $string ), $iv_size ), $encrypt_method,
$secret_key,
0,
$decrypt_iv ) );
if ( false == $output ) {
$output = $string;
}
}
return $output;
}
The above code fails to decrypt the text if the content is ONLY a zero character!!!
It returns the encoded string that encrypt produced.
Any ideas?
if ( false == $output ) {
$output = $string;
}
This checks if the output is "falsy" and if it is it sets $output back to the original encrypted string. "0" is falsy. Replace the check with
if ( false === $output ) {
To check if the output is actually false and not just falsy.
For a better explanation of what is and isn't falsy, see this documentation page.