问题
I am trying to login to a website (Steam) which encrypts the plaintext password using Javascript RSA as to send the ciphertext in the POST request as a parameter. I am having trouble correctly converting the Javascript RSA from Javascript to PHP.
When I attempt to send the ciphertext password created with any of my PHP script's to the website, I get an incorrect login indicating that something is incorrect somewhere in my encryption process.
When sending an actual request from the browser and recording the request with Fiddler, the bit length of the modulus was always the same as that of the ciphertext. This can also be deduced from the Javascript function pkcs1pad2. This was one of the criteria that I looked for when trying to check if the encryption was correct.
Using the same public key and plaintext will not always result in the same ciphertext as when using pkcs1, random bytes are padded onto the start of the plaintext until the length is the same as the modulus. Therefore it is not possible to compare against a correct ciphertext obtained through the browser.
Do Javascript's modPowInt($exponent, $modulus) and PHP's modPow($exponent, $modulus) perform different calculations as PHP RSA (2) doesn't work although it seems to be exactly the same as Javascript RSA?
Javascript RSA - What needs to be converted to PHP
var RSAPublicKey = function($modulus_hex, $encryptionExponent_hex) {
this.modulus = new BigInteger( $modulus_hex, 16);
this.encryptionExponent = new BigInteger( $encryptionExponent_hex, 16);
};
var Base64 = {
base64: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
encode: function($input) {
if (!$input) {
return false;
}
var $output = "";
var $chr1, $chr2, $chr3;
var $enc1, $enc2, $enc3, $enc4;
var $i = 0;
do {
$chr1 = $input.charCodeAt($i++);
$chr2 = $input.charCodeAt($i++);
$chr3 = $input.charCodeAt($i++);
$enc1 = $chr1 >> 2;
$enc2 = (($chr1 & 3) << 4) | ($chr2 >> 4);
$enc3 = (($chr2 & 15) << 2) | ($chr3 >> 6);
$enc4 = $chr3 & 63;
if (isNaN($chr2)) $enc3 = $enc4 = 64;
else if (isNaN($chr3)) $enc4 = 64;
$output += this.base64.charAt($enc1) + this.base64.charAt($enc2) + this.base64.charAt($enc3) + this.base64.charAt($enc4);
} while ($i < $input.length);
return $output;
},
decode: function($input) {
if(!$input) return false;
$input = $input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
var $output = "";
var $enc1, $enc2, $enc3, $enc4;
var $i = 0;
do {
$enc1 = this.base64.indexOf($input.charAt($i++));
$enc2 = this.base64.indexOf($input.charAt($i++));
$enc3 = this.base64.indexOf($input.charAt($i++));
$enc4 = this.base64.indexOf($input.charAt($i++));
$output += String.fromCharCode(($enc1 << 2) | ($enc2 >> 4));
if ($enc3 != 64) $output += String.fromCharCode((($enc2 & 15) << 4) | ($enc3 >> 2));
if ($enc4 != 64) $output += String.fromCharCode((($enc3 & 3) << 6) | $enc4);
} while ($i < $input.length);
return $output;
}
};
var Hex = {
hex: "0123456789abcdef",
encode: function($input) {
if(!$input) return false;
var $output = "";
var $k;
var $i = 0;
do {
$k = $input.charCodeAt($i++);
$output += this.hex.charAt(($k >> 4) &0xf) + this.hex.charAt($k & 0xf);
} while ($i < $input.length);
return $output;
},
decode: function($input) {
if(!$input) return false;
$input = $input.replace(/[^0-9abcdef]/g, "");
var $output = "";
var $i = 0;
do {
$output += String.fromCharCode(((this.hex.indexOf($input.charAt($i++)) << 4) & 0xf0) | (this.hex.indexOf($input.charAt($i++)) & 0xf));
} while ($i < $input.length);
return $output;
}
};
var RSA = {
getPublicKey: function( $modulus_hex, $exponent_hex ) {
return new RSAPublicKey( $modulus_hex, $exponent_hex );
},
encrypt: function($data, $pubkey) {
if (!$pubkey) return false;
$data = this.pkcs1pad2($data,($pubkey.modulus.bitLength()+7)>>3);
if(!$data) return false;
$data = $data.modPowInt($pubkey.encryptionExponent, $pubkey.modulus);
if(!$data) return false;
$data = $data.toString(16);
if(($data.length & 1) == 1)
$data = "0" + $data;
return Base64.encode(Hex.decode($data));
},
pkcs1pad2: function($data, $keysize) {
if($keysize < $data.length + 11)
return null;
var $buffer = [];
var $i = $data.length - 1;
while($i >= 0 && $keysize > 0)
$buffer[--$keysize] = $data.charCodeAt($i--);
$buffer[--$keysize] = 0;
while($keysize > 2)
$buffer[--$keysize] = Math.floor(Math.random()*254) + 1;
$buffer[--$keysize] = 2;
$buffer[--$keysize] = 0;
return new BigInteger($buffer);
}
};
PHP RSA (1) This was my initial attempt at solving the encryption issue. The Bit Length of the ciphertext is consistant with that of the Modulus (2048. The ciphertext however, resulted in an incorrect login.
include 'phpseclib/Math/BigInteger.php';
include 'phpseclib/Crypt/RSA.php';
function encrypt($data, $mod, $exp)
{
$rsa = new Crypt_RSA();
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
$rsa->publicExponent = new Math_BigInteger($exp, 16);
$rsa->modulus = new Math_BigInteger($mod, 16);
$rsa->publicKeyFormat = CRYPT_RSA_PUBLIC_FORMAT_PKCS1;
$rsa->loadKey($rsa->getPublicKey());
return urlencode(base64_encode($rsa->encrypt($data)));
}
PHP RSA (2) I tried to convert the Javascript RSA script to PHP. The Bit Length is inconsistent with that of the Modulus (2472 not 2048)
include 'phpseclib/Math/BigInteger.php';
include 'phpseclib/Crypt/RSA.php';
function encrypt($data, $mod, $exp){
$mod = new Math_BigInteger($mod,16);
$exp = new Math_BigInteger($exp,16);
if($exp == null || $mod == null) return false;
$data = pkcs1pad2($data, (strlen($mod->toBits())+7)>>3);
if($data == null) return false;
$data = $data->modPow($exp,$mod);
if($data == null) return false;
$data = $data->toString();
if((strlen($data) & 1) == 1)
$data = "0" . $data;
return urlencode(base64_encode(hex2bin($data)));
}
function pkcs1pad2($data, $keysize){
if($keysize < strlen($data) + 11)
return null;
$buffer = array();
$i = strlen($data)-1;
while($i >= 0 && $keysize > 0)
$buffer[--$keysize] = $data[$i--];
$buffer[--$keysize] = 0;
while($keysize > 2)
$buffer[--$keysize] = rand(0,255);
$buffer[--$keysize] = 2;
$buffer[--$keysize] = 0;
return new Math_BigInteger(bin2hex(implode('',$buffer)), 16);
}
PHP RSA (3) This is the final iteration of my code. I tried to pad missing bits to make the Bit Length the same as that of the Modulus (2048) and succeeded. The ciphertext password is still considered incorrect by the website though.
include('Crypt/RSA.php');
include('Math/BigInteger.php');
function hex_to_binary($hex) {
$binary = '';
for ($i = 0; $i < strlen($hex); $i += 2) {
$hexChunk = substr($hex, $i, 2);
$intChunk = hexdec($hexChunk);
$binaryChunk = decbin($intChunk);
$binaryChunk = str_pad($binaryChunk, 8, '0', STR_PAD_LEFT);
$binary .= $binaryChunk;
}
return $binary;
}
function bytes_to_binary($bytes) {
$binary = '';
foreach($bytes as $integer) {
$byte = decbin($integer);
$byte = str_pad($byte, 8, '0', STR_PAD_LEFT);
$binary .= $byte;
}
return $binary;
}
function binary_to_text($binary) {
$text = '';
$binaryLength = strlen($binary);
for ($i = 0; $i < $binaryLength; $i += 8) {
$binaryChunk = substr($binary, $i, 8);
$characterCode = bindec($binaryChunk);
$character = chr($characterCode);
$text .= $character;
}
return $text;
}
function getPublicKey($modulusHex, $exponentHex) {
$publicKey = Array(
'modulus' => $modulusHex,
'exponent' => $exponentHex
);
return $publicKey;
}
function pkcs1pad2($data, $publicKey) {
// Convert Modulus from Hex to Binary
$modulusBinary = hex_to_binary($publicKey['modulus']);
// Get Bytes of Modulus
$modulusInteger = new Math_BigInteger($modulusBinary, 2);
$modulusBytes = ceil(strlen($modulusInteger->toBits()) / 8);
// Bytes in the Modulus must be 11 Bytes longer than Bytes in the Password (UTF-8 => 8 Bytes per Character)
if($modulusBytes < strlen($data) + 11) {
// Otherwise Encryption is impossible so Return Null
return null;
};
// Array to Store Sequence of Bytes in the Padded Password
$buffer = array();
// Variables to Hold Current Position of Bytes and Characters
$currentModulusByte = $modulusBytes;
$currentDataCharacter = strlen($data) - 1;
// Insert Password into End of Buffer
while($currentDataCharacter >= 0 && $currentModulusByte > 0) {
$buffer[--$currentModulusByte] = ord($data[$currentModulusByte--]);
};
// Insert 0 as the Next Last Value of Buffer
$Buffer[--$currentModulusByte] = 0;
// Insert Random Bytes into Buffer until the First 2 Bytes
while($currentModulusByte > 2) {
$buffer[--$currentModulusByte] = rand(1,255);
};
// Insert 0 and 2 as the First 2 Bytes in the Sequence
$buffer[--$currentModulusByte] = 2;
$buffer[--$currentModulusByte] = 0;
// Math_BigInteger() doesn't accept an Array of Bytes so convert it to a Binary string
$paddedModulusBinary = bytes_to_binary($buffer);
// Convert Binary to BigInteger using a Base 2
$paddedModulusInteger = new Math_BigInteger($paddedModulusBinary, 2);
return $paddedModulusInteger;
}
// Copy of the Encrypt function
function encrypt($data, $publicKey) {
// Make Sure that the Public Key is not Null
if(!$publicKey) {
return false;
};
// Pad the Data for Encryption
$paddedData = pkcs1pad2($data, $publicKey);
// Make Sure that the Padded Data is not Null
if(!$paddedData) {
return false;
};
// Encrypt the Padded Data using the Public Key
$exponentBinary = hex_to_binary($publicKey['exponent']);
$exponentBigInt = new Math_BigInteger($exponentBinary, 2);
$modulusBinary = hex_to_binary($publicKey['modulus']);
$modulusBigInt = new Math_BigInteger($modulusBinary, 2);
$encryptedData = $paddedData->modPow($exponentBigInt, $modulusBigInt);
// Make Sure that the Encrypted Data is not Null
if(!$encryptedData) {
return false;
}
// Convert the Encrypted Data to Binary
$encryptedBinaryData = $encryptedData->toBits();
// Pad Empty Bits onto the Start of the Encrypted Binary Data
$modulusBitLength = strlen($publicKey['modulus']) * 4;
$encryptedBinaryData = str_pad($encryptedBinaryData, $modulusBitLength, '0', STR_PAD_LEFT);
// Convert Binary to Text
$textData = binary_to_text($encryptedBinaryData);
// Encode Binary with Base64
$base64EncodedData = base64_encode($textData);
// Encode Base64 for Url
$urlEncodedData = urlencode($base64EncodedData);
return $urlEncodedData;
}
Any help at all would be much appreciated.
This question has come up in the past but has never been properly answered. Hopefully we will have some more luck this time.
回答1:
My first thought... you may need to do define('CRYPT_RSA_PKCS15_COMPAT', true)
. phpseclib - which you're using on the PHP side - uses a padding technique defined in PKCS1 v2.0+, which differs slightly from the padding technique described in PKCS1 v1.5. Doing define('CRYPT_RSA_PKCS15_COMPAT', true)
before you do the encryption might help.
Failing that, it might be worthwhile to try to do $rsa->publicExponent = new Math_BigInteger($exp, -16)
I'll try to play around with this some more when I get home from work..
回答2:
Instead of rolling your own solution, you should consider using the openssl_{public,private}_{encrypt,decrypt} functions.
来源:https://stackoverflow.com/questions/34399817/converting-rsa-encryption-from-javascript-to-php