How can I validate a X-HW-SIGNATURE in PHP?
The documentation for request parameters reads:
Message header signature, which is mandatory, indicating the
signature information sent to your server that receives uplink messages.
There's also example data:
timestamp=1563105451261; nonce=:; value=E4YeOsnMtHZ6592U8B9S37238E+Hwtjfrmpf8AQXF+c=
The keys are:
timestamp: standard Unix timestamp
nonce: colon
value: character string to be encrypted
This here is the part which I don't understand:
timestamp + nonce + Uplink message content: obtained after the encryption using the set password in HMAC-SHA256 algorithm and encoding in Base64.
How can I validate the message payload against the header signature?
What I've tried so far basically is:
private function parse_request_body(): void {
$this->rawBody = stream_get_contents(STDIN);
if (isset($_SERVER['X-HW-SIGNATURE']) && !empty($_SERVER['X-HW-SIGNATURE'])) {
if (! $this->hmac_verify( $this->rawBody, $_SERVER['X-HW-SIGNATURE'] )) {
// spoof message
}
}
}
private function hmac_verify( string $payload, string $signature ): bool {
// the problem obviously lies here ...
return true;
}
This is how i would go about verifying the signature. From my understanding from the doc. However it isn't 100% clear as they do not provide an example, which is a shame...
You should have (or be able to create one) a secret key within your Huawei account somewhere.
private function hmac_verify( string $payload, string $signature ): bool
{
$secretKey = 'yoursecretkey';
$parsedSignature = str_replace(';', '&', $signature); //'timestamp=1563105451261& nonce=:& value=E4YeOsnMtHZ6592U8B9S37238E+Hwtjfrmpf8AQXF+c='
parse_str($parsedSignature, $signatureParts);
// $signatureParts
//
// array(3) {
// ["timestamp"]=>
// string(13) "1563105451261"
// ["nonce"]=>
// string(1) ":"
// ["value"]=>
// string(44) "E4YeOsnMtHZ6592U8B9S37238E Hwtjfrmpf8AQXF c="
// }
$signed = hash_hmac("sha256", $signatureParts['timestamp'] + $signatureParts['nonce'] + $payload, $secretKey);
return base64_encode($signed) === $signatureParts['value'];
}
On another page of the documentation (X-HUAWEI-CALLBACK-ID), I've found a similar description:
Base64-encoded string that has been HMAC-SHA256 encrypted using the callback key. The string before encryption consists of the value of timestamp, value of nonce, and callback user name, without plus signs.
And here it's being described how to send push.hcm.upstream messages on Android. Sending an upstream message might be the best chance to obtain the payload, in order to validate a signature. The server-side procedure upon send as following:
When receiving the uplink message, the Push Kit server will:
Combine the receiving timestamp, colon (:), and uplink message into a character array to be encrypted (for example, 123456789:your_data).
Encrypt the character array in HMAC-SHA256 mode using an HMAC signature verification key, and encode the encrypted result in Base64 to generate a signature.
Transfer the signature and timestamp information to your app server through the X-HW-SIGNATURE and X-HW-TIMESTAMP fields in the HTTPS request header.
Your app server needs to use the HTTPS request header and HMAC signature verification key to verify the validity of the uplink message.
Whatever "an HMAC signature verification key" may be; placeholder your_data sounds alike, as if (likely not yet base64 encoded) $payload->data would have been used to generate the signature:
/** Concatenate the input string, generate HMAC hash with SHA256 algorithm, then encode as base64. */
private function generate_signature( int $timestamp, string $nonce, string $data_str, string $secret_key): string {
$input = $timestamp.$nonce.$data_str;
$hmac = hash_hmac( 'sha256', $input, $secret_key );
return base64_encode( $hmac );
}
/** Convert the received signature string to object. */
private function to_object( string $signature ): stdClass {
$input = str_getcsv( $signature, '; ' );
$data = new stdClass();
$data->timestamp = (int) str_replace('timestamp=', '', $input[0]);
$data->nonce = (string) str_replace( ' nonce=', '', $input[1]);
$data->value = (string) str_replace( ' value=', '', $input[2]);
return $data;
}
public function hmac_verify( string $raw_body, string $secret_key, string $signature ): bool {
/* Extract data-string from the raw body. */
$payload = json_decode( $raw_body );
$data_str = base64_decode( $payload->data );
/* Convert the received signature string to object. */
$signature = $this->to_object( $signature );
/* Generate a signature which to compare to. */
$generated = $this->generate_signature( $signature->timestamp, $signature->nonce, $data_str, $secret_key);
/* Compare the generated with the received signature. */
return $generated === $signature->value;
}
Need to test this once with an actual $_POST ...
The "HMAC signature verification key" (per web-hook) can be obtained from the PushKit console:
Thank you for providing the information regarding this issue. We are very sorry that it brings you inconvinience and are now organizing R&D team to supplement the sample code.
The X-HW-SIGNATURE field is used to check whether the message is from Huawei service.
Usage:
Timestamp + nonce + Uplink message content are combined into a character string. Use the configured HMAC HMAC-SHA256 algorithm and encoding in Base64 to compare the obtained value with the value sent by the push service. If they are the same, the message is from the push service, and you do not need to parse the specific value of this field.
Related
I am using PHP to encrypt a string using a public key. I then pass the private key to a Node.js microservice for decryption. I am getting a successful decryption, but there is strange encoded characters before the true decrypted value.
PHP Generate Keys & Encrypt Value
$user_passphrase = '***'
$string = '123ABC';
// Generate key pair
$pkey = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
// Extract private key
openssl_pkey_export($pkey, $private_key, $user_passphrase);
// Base64 encode private key
$private_key = base64_encode($private_key);
// Extract public key
$pkey_details = openssl_pkey_get_details($pkey);
// Base64 encoded public key
$public_key = base64_encode($pkey_details['key']);
// Encrypt value
openssl_public_encrypt($string, $encrypted_value, base64_decode($public_key));
var_dump('Encrypted Value: ' . base64_encode($encrypted_value));
Node.js Decrypt Value
const crypto = require('crypto');
function decrypt (value, privateKey, passphrase) {
value = Buffer.from(value, 'base64');
privateKey = Buffer.from(privateKey, 'base64');
return crypto.privateDecrypt({
key: privateKey,
passphrase: passphrase,
padding: crypto.constants.RSA_NO_PADDING
}, value).toString('utf8');
}
const decryptedValue = decrypt(encryptedString, encryptedPrivateKey, userPassphrase);
console.log('Decrypted Value:' + decryptedValue);
When I run the php script, I pass the necessary values to the Node.js script. When the Node.js script decodes the value, the correct decoded value (123ABC) is at the end of a long string with strange encoding characters. Why am I seeing these strange characters?
�R^�z���p��LUZ��ϓ)�V'�5I��T�Z&jzD���TM�d��U�#��3�D�z�ו"��k��I��p���D�?����Oe���Ȯzl�Mss�L��;�j���yX�j�ŭ�]�� 123ABC
I'm trying to validate a Google Play/Android In App Product consumed purchase server-side using PHP. I get a response back with a valid receipt, but there are two confusing issues:
The productId is always null
If I change the the $productId in the sample below to an invalid ID, it will return the exact same response. This seems to be the case for literally any string.
Here is my sample code:
$purchaseToken = 'TEST purchaseToken FROM ANDROID APP';
$appId = 'com.example.myapp';
$productId = 'com.example.myapp.iap1';
$googleClient = new \Google_Client();
$googleClient->setScopes([\Google_Service_AndroidPublisher::ANDROIDPUBLISHER]);
$googleClient->setApplicationName($appId);
$googleClient->setAuthConfig(__DIR__ . '/gp-service.json');
$googleAndroidPublisher = new \Google_Service_AndroidPublisher($googleClient);
$purchase = $googleAndroidPublisher->purchases_products->get($appId, $productId, $purchaseToken);
If I dump out $purchase, I get:
=> Google_Service_AndroidPublisher_ProductPurchase {
+acknowledgementState: 1,
+consumptionState: 1,
+developerPayload: "",
+kind: "androidpublisher#productPurchase",
+obfuscatedExternalAccountId: null,
+obfuscatedExternalProfileId: null,
+orderId: "GPA.XXXX-XXXX-XXXX-XXXXX",
+productId: null,
+purchaseState: 0,
+purchaseTimeMillis: "1602771022178",
+purchaseToken: null,
+purchaseType: 0,
+quantity: null,
+regionCode: "US",
}
Does anyone know what I am doing wrong here? It doesn't seem to be validating the productId on its end nor does it provide me the data I would need to validate it on my end, meaning I have no way of validating this IAP right now.
https://issuetracker.google.com/issues/169870112
I've found on google issue tracker that ignoring productId is an intended behaviour. I've wrote post asking if the null that we receive in response is also an intended behaviour. I hope not because as you wrote if productId will be always null we have no way of fully validating IAP right now.
$purchaseToken was undefined in Josh's answer.
<?php
$appId = "com.example.myapp";
$serviceAccountJson = __DIR__ . "/gp-service.json";
// this is the raw receipt data received on the device from Google Play; this example is obfuscated and only shows the keys for sensitive fields
$googlePlayReceipt =
'{"productId": "com.example.myapp.iap1","transactionDate": 1602714720893,"transactionReceipt": "","purchaseToken": "","transactionId": "","dataAndroid": "","signatureAndroid": "","isAcknowledgedAndroid": false,"autoRenewingAndroid": false,"purchaseStateAndroid": 1}';
// decode the json to an array we can use
$decodedGooglePlayReceipt = json_decode($googlePlayReceipt, true);
// the data that was signed for verification purposes
$data = $decodedGooglePlayReceipt["transactionReceipt"];
// the signature that was used to sign the $data
$signature = $decodedGooglePlayReceipt["signatureAndroid"];
// The "Base64-encoded RSA public key" for your app, taken from the Google Play Console
// In the Classic Console: Your App -> Development Tools -> Services & APIs -> Licensing & in-app billing
// In the New Console: Your App -> Monetize -> Monetization Setup -> Licensing
$base64EncodedPublicKeyFromGoogle = "################";
// Convert the key into the normal public key format
// Just need to split the base64 key into 64 character long lines and add the usual prefix/suffix
$openSslFriendlyKey =
"-----BEGIN PUBLIC KEY-----\n" .
chunk_split($base64EncodedPublicKeyFromGoogle, 64, "\n") .
"-----END PUBLIC KEY-----";
// Convert the key to the openssl key ID that openssl_verify() expects
// I'm unsure if this step would be necessary on all platforms
$publicKeyId = openssl_get_publickey($openSslFriendlyKey);
// Use openssl_verify() to verify the $signature (which has to be base64 decoded!) against the $data using the public key we have
$result = openssl_verify(
$data,
base64_decode($signature),
$publicKeyId,
OPENSSL_ALGO_SHA1
);
if ($result === 0) {
throw new Exception("Invalid receipt");
}
if ($result !== 1) {
throw new Exception(openssl_error_string());
}
// receipt data is valid. now let's grab the productId and purchaseToken
$decodedData = json_decode($data, true);
$purchasedProductId = decodedData["productId"];
$purchaseToken = decodedData["purchaseToken"];
// now we'll verify that the receipt is valid for our account
try {
$googleClient = new \Google_Client();
$googleClient->setScopes([
\Google_Service_AndroidPublisher::ANDROIDPUBLISHER,
]);
$googleClient->setApplicationName($appId);
$googleClient->setAuthConfig($serviceAccountJson);
$googleAndroidPublisher = new \Google_Service_AndroidPublisher(
$googleClient
);
$purchase = $googleAndroidPublisher->purchases_products->get(
$appId,
$purchasedProductId,
$purchaseToken
);
} catch (Throwable $exception) {
// this means the receipt data is unable to be validated by Google
throw new Exception("Invalid receipt");
}
After seeing Deusald's response here and starting to type up a response to the Google Issues ticket complaining about it, I had an epiphany: Google is just validating that the transaction is valid for your account and expects you to validate the receipt data server-side. They include a base64 encoded RSA SHA1 signature in the receipt data and the original data they used to create that signature, so they give you everything you need to accomplish this.
The below snippet accomplishes that verification for PHP, but it should be easily portable to other languages:
<?php
// our app's bundle id
$appId = 'com.example.myapp';
// the location of a Service Account JSON file for a Service account that has access to the "Financial Data" permissions in the Play Console
$serviceAccountJson = __DIR__ . '/gp-service.json';
// this is the raw receipt data received on the device from Google Play; this example is obfuscated and only shows the keys for sensitive fields
$googlePlayReceipt = '{"productId": "com.example.myapp.iap1","transactionDate": 1602714720893,"transactionReceipt": "","purchaseToken": "","transactionId": "","dataAndroid": "","signatureAndroid": "","isAcknowledgedAndroid": false,"autoRenewingAndroid": false,"purchaseStateAndroid": 1}';
// decode the json to an array we can use
$decodedGooglePlayReceipt = json_decode($googlePlayReceipt, true);
// the data that was signed for verification purposes
$data = $decodedGooglePlayReceipt['transactionReceipt'];
// the signature that was used to sign the $data
$signature = $decodedGooglePlayReceipt['signatureAndroid'];
// The "Base64-encoded RSA public key" for your app, taken from the Google Play Console
// In the Classic Console: Your App -> Development Tools -> Services & APIs -> Licensing & in-app billing
// In the New Console: Your App -> Monetize -> Monetization Setup -> Licensing
$base64EncodedPublicKeyFromGoogle = '################';
// Convert the key into the normal public key format
// Just need to split the base64 key into 64 character long lines and add the usual prefix/suffix
$openSslFriendlyKey = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($base64EncodedPublicKeyFromGoogle, 64, "\n") . "-----END PUBLIC KEY-----";
// Convert the key to the openssl key ID that openssl_verify() expects
// I'm unsure if this step would be necessary on all platforms
$publicKeyId = openssl_get_publickey($openSslFriendlyKey);
// Use openssl_verify() to verify the $signature (which has to be base64 decoded!) against the $data using the public key we have
$result = openssl_verify($data, base64_decode($signature), $publicKeyId, OPENSSL_ALGO_SHA1);
if ($result === 1) {
// receipt data is valid. now let's grab the productId and purchaseToken
$decodedData = json_decode($data, true);
$purchasedProductId = decodedData['productId'];
$purchaseToken = decodedData['purchaseToken'];
// now we'll verify that the receipt is valid for our account
try {
$googleClient = new \Google_Client();
$googleClient->setScopes([\Google_Service_AndroidPublisher::ANDROIDPUBLISHER]);
$googleClient->setApplicationName($appId);
$googleClient->setAuthConfig($serviceAccountJson);
$googleAndroidPublisher = new \Google_Service_AndroidPublisher($googleClient);
$purchase = $googleAndroidPublisher->purchases_products->get($appId, $purchasedProductId, $purchaseToken);
} catch (Throwable $exception) {
// this means the receipt data is unable to be validated by Google
throw new Exception('Invalid receipt');
}
} elseif ($result === 0) {
throw new Exception('Invalid receipt');
} else {
throw new Exception(openssl_error_string());
}
Assuming that I received token after managed to login through openid-connect
http://xxxxxx/auth/realms/demo/protocol/openid-connect/token
{
"access_token": "xxxxxx",
"expires_in": 600,
"refresh_expires_in": 1800,
"refresh_token": "xxxxxx",
"token_type": "bearer",
"not-before-policy": xxxx,
"session_state": "xxxxx",
"scope": "email profile"
}
Is there any ways on how to decode the payload of the jwt tokens just like the https://jwt.io/ did , using PHP? Thank you.
You can use this library https://github.com/firebase/php-jwt.
Why do you want to decode the access_token though? Usually it's the id_token that gets decoded so that the client can verify the end-user's identity. The process of decoding requires the JWT to have its signature verified.
You can use the library I mentioned above. The steps are easy. You need:
The JWT
Secret Key/ Public Key
Algorithm used to encode the JWT
The following snippet is used to decode + verify a JWT. It uses HS256 so a secret key must be in the possession of the client:
$decoded = JWT::decode($jwt, $key, array('HS256'));
If you want to decode a JWT without verifying its signature (unsafe), you can create a function that separates each of the JWT section: header, body, and signature, and base64url decode it. Like so:
// Pass in the JWT, and choose which section. header = 0; body = 1; signature = 2
public function decodeJWT($jwt, $section = 0) {
$parts = explode(".", $jwt);
return json_decode(base64url_decode($parts[$section]));
}
EDIT if you're decoding + verifying an id_token which uses assymetric algorithm e.g. RSA256, RSA384 etc, you need the public key. OpenID Connect defines a JWK Set endpoint (/.well-known/jwks.json), which lists the public keys in JWK format. You can hit that endpoint and save the response in an array. In order to find which public key was used, the JWK has a kid claim/ property. Which represents the key id, the identifier of the public key. You can decode your id_token and grab its header using :
$header = decodeJWT($id_token, 0);
Then you can pass the header to the function below to get the key that was used to encode the id_token. Parameter $keys holds the JWK Set response:
function getIdTokenKey($keys, $header) {
foreach ($keys as $key) {
if ($key->kty == 'RSA') {
if (!isset($header->kid) || $key->kid == $header->kid) {
return $key;
}
}
}
throw new Exception("key not found");
}
$key = getIdTokenKey($keys, $header);
Finally call the decode function, assume it's using RSA256:
$decoded = JWT::decode($id_token, $key, array('RSA256'));
Edit(2) On another note it's the same process to decode any JWT, be it an access token, id token, or arbitrary data being passed to different entities in a server environment.
I can't find any information on what algorithm to use to decode WooCommerce webhook field X-Wc-Webhook-Signature in PHP. Does anyone know how to decode it?
Thanks!
Expanding on the current answers, this is the PHP code snippet you need:
$sig = base64_encode(hash_hmac('sha256', $request_body, $secret, true));
Where $secret is your secret, $request_body is the request body, which can be fetched with file_get_contents('php://input');
The $sig value should then be equal to the X-Wc-Webhook-Signature request header.
To expand on the laravel solution this is how I created middleware to validate the incoming webhook.
Create middleware. The application that I am using keeps the WooCommerce consumer key and secret in a table assigned to a given store.
class ValidateWebhook
{
/**
* Validate that the incoming request has been signed by the correct consumer key for the supplied store id
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
$signature = $request->header('X-WC-Webhook-Signature');
if (empty($signature)) {
return response(['Invalid key'], 401);
}
$store_id = $request['store'];
$consumer_key = ConsumerKeys::fetchConsumerSecretByStoreId($store_id);
$payload = $request->getContent();
$calculated_hmac = base64_encode(hash_hmac('sha256', $payload, $consumer_key, true));
if ($signature != $calculated_hmac) {
return response(['Invalid key'], 401);
}
return $next($request);
}
}
Register the middleware in Kernel.php
'webhook' => \App\Http\Middleware\ValidateWebhook::class,
Protect the webhook route with the middleware
Route::post('webhook', 'PrintController#webhook')->middleware('webhook');
Here is my solution for this question. You need to generate a sha256 hash for the content that was sent [payload] and encode it in base64. Then just compare the generated hash with the received one.
$secret = 'your-secret-here';
$payload = file_get_contents('php://input');
$receivedHeaders = apache_request_headers();
$receivedHash = $receivedHeaders['X-WC-Webhook-Signature'];
$generatedHash = base64_encode(hash_hmac('sha256', $payload, $secret, true));
if($receivedHash === $generatedHash):
//Verified, continue your code
else:
//exit
endif;
According to the docs:
"A base64 encoded HMAC-SHA256 hash of the payload"
I'm guessing that the payload in this instances is the secret you've supplied in the webhook properties.
Source: https://woocommerce.github.io/woocommerce-rest-api-docs/v3.html#webhooks-properties
EDIT
Further digging indicates that the payload is the body of the request.
So you'd use a HMAC library to create a sha256 hash of the payload using your webhook secret, and then base64 encode the result, and do the comparison with X-Wc-Webhook-Signature and see if they match.
You can generate a key pair from the following URL:
/wc-auth/v1/authorize?app_name=my_app&scope=read_write&user_id=<a_unique_id_that_survives_the_roundtrip>&return_url=<where_to_go_when_the_flow_completes>&callback_url=<your_server_url_that_will_receieve_the_following_params>
These are the params the callback URL receieves in the request body:
{
consumer_key: string,
consumer_secret: string,
key_permissions: string,
user_id: string,
}
Here's my laravel / lumen function to verify the webhook request, hope it helps someone!
private function verifyWebhook(Request $request) {
$signature = $request->header('x-wc-webhook-signature');
$payload = $request->getContent();
$calculated_hmac = base64_encode(hash_hmac('sha256', $payload, 'WOOCOMMERCE_KEY', true));
if($signature != $calculated_hmac) {
abort(403);
}
return true;
}
I've been searching all day long(aprox. 12 hours) how to deal with this and couldn't find anything to work for me so I decided to make another question on the topic.
My setup:
Taking credentials from user
Encrypt them using RSA public key and then base64
Send to server
base64_decode, decrypt and check credentials
This is the code I use for encryption on Android:
public class Encrypter {
public static PublicKey loadPublicKey(Context context) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
InputStream is = context.getAssets().open("public_key.txt");
byte[] bytes = new byte[is.available()];
is.read(bytes);
PublicKey publicKey = KeyFactory.getInstance(RSA).generatePublic(
new X509EncodedKeySpec(Base64.decode(bytes)));
return publicKey;
}
private static byte[] enc(String text, PublicKey pubRSA) throws Exception {
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, pubRSA);
return cipher.doFinal(text.getBytes("UTF-8"));
}
public final static String encrypt(String text, PublicKey uk) {
try {
return Base64.encodeBytes(enc(text, uk));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
And how I use it:
String username = "admin";
PublicKey pubk = Encrypter.getPublicKey(this);
username = Encrypter.encrypt(username, pubk);
This is my public key without START and END:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsfPDQ7tRqUw8oTPcNG3GR9OyhWnrtQXj2gmzTKdLncPIuolG1GTjYyZO25+cHSgHBlFmc20cKO0uasveZWCwuBTmMY3kVYtVblxUbHmHakLc2CRsKlCA3GAU/OAvfQfzcRUE0O1R138XcTydDGNgWKthqePJz97sEtg8nY55imVUTfRJPOeMvn1/vQQY50OTGniyoI+sh66P/0xpjrZQdIKaNADD5tQbDmrrJjoocC/GuwqBizi7rmU/p/9udNj5hCKqxL6PmejDsHss+UrhBx5t1/iem2yEBhvuOPDyvBPn/ZqAEKeAUKyf+Z5d3XQAlzJq5UjliRM2IA/K6uUBCQIDAQAB
My problem is that if I encrypt it using Android and then Base64.encode it, I get a different string than if I encrypt from PHP using the same public key
I tried encrypting the work "admin" on both sides and here's what I got:
String got from Android:
hF8ykDKLYVJOnHE5Uswq0+BasIRqTLnFIjvy2rLxfe/oDJ0GbhjTjoizNuSk2grKbAgkJqFN5uAFuhkqqjyxoSJlJNsQi5QRis9FDUIm1iAhjSD8olTBFky+q1pqYQsQ/Cj+9qSVTnoKpB2oJyeEk2Zx7mYegHKT/yItDtsSLa7fURaxygp1osj0Nz8pas21zXgMIyG2wKARG9IlxdBo4Vl2nj7iKwPCkHMrSeXzFjDsKOkwBzMoPuUUGSFUZ0QbL+b/Ha+Rgdb7oItzTvBfHsoL9m91j20NhqapKRYkJ2pPUhPPVDZtTzO/JPKK4ndzg32w7jKqb9zinOBcilQdGg==
String from PHP:
oh8fNeY8FwPqUkvJhFQr/2IPgdj7XEUNHjc7+KZwRGot+4DIQWtxv3N4UtzbpvkwcgI/kUjXZOz+mNSzvTEVpmZprOWBow/zlbCO7tLgH2Q131gATZdGxPEgOIVbTOWQkXL+d8x+jODnPhaXb8vUB2boQmd70ifBAq2C5mMCGPeA/gRwNquwdEG62W4zvaeXzXc6sXCXXvE6cgaWLOhZWFnLyo2ulFrkGk9XXOaWoS3HYnS35n8xHxulSEeAJOmGgEd56cSbDIlJrD9H5k6mb6PX0/eLoC7J9vdBhdM8nJVviL6NxtiOtDTCWVFb4k9il8Sksz8eascFM8yAB0KvRw==
For encryption in PHP I use phpseclib under Laravel 5.2 framework
Method to check credentials
public function creds_valid(Request $request) {
$inputs = $request->all();
$username = $inputs['username'];
$password = $inputs['password'];
$private_key = file_get_contents(storage_path() . "/phpseclib/public_key.txt");
$rsa = new RSA();
$rsa->loadKey($private_key);
$username = $rsa->decrypt(base64_decode($username));
$password = $rsa->decrypt(base64_decode($password));
$credentials = [
'username' => $username,
'password' => $password,
// 'active' => 1
];
return Auth::guard()->attempt($credentials);
}
Why aren't the two algorithms output the same string on encryption and base64? I suspect some encoding issues but I can't figure them out. Tried forcing UTF-8 on both but the result is the same.. Any ideas?
Thanks!
EDIT:
Using this base64 library for Java:
https://sourceforge.net/projects/iharder/files/base64/2.3/
The default one from Android gives the same results so, yeah.. :/
EDIT 2:
First comment suggested checking to see if any party changes the output on concurrent executions and weirdly, yes.. The PHP one.. Why?
This is an output from php artisan tinker
base64_encode($rsa->encrypt('admin'))
=> "T3dBTadyEjvSM96lCwELNzXHNh5bEJtW6QvgdrVT0wZ2KFq8Cs+s7/+IjSrISlNC+ygF+XPhYjEUHa+FSqzgT68KftcVQYL84w8Thbiy6ElLqs7WAbLAaKk10kBqEPNtI0jItOJFXGA07SsQu6g5+OUWfEFShBPg8uSxSYPsjhvATpw6lLCRtVQYqAL1MUwwmaho/ktih5UJgpiCYdwpa0ROiPboKloO0CFOAkR+5rybSLH73p3Fxf1y46qgA3BoPenillkMW8kgv1/bFj1rpRVi2Ca3ioMQt0fOPGchoe3ikz5I6IrrAK0aYhwj3kpoQ2+doLUV+EKXs8iFkA+cjQ=="
>>> base64_encode($rsa->encrypt('admin'))
=> "XJZrhFpw1UsBEC2yKMv0PVQ/H0vvBSXEZEsjQ4vlvjgktifCGAPsac3zwarlV2+TOoNKhn7nCqWer2Gz60UyoHS0xYyNvt/i3Ogsn2ccxhpKRtrB2CEMJOzSdLoUe+4JEObdU1b6SR4ysFbFEjFvo+zWUAfT7i2vM9bdZkwcrL6S2a4bBeWk3t2l91URatggzTOBFtxUB4zHTKyo1SSuR6uqm2q/Jbakj07e64ZZQmWiKY4inDZNgaVVLE+wNq++J4aLwRhSZG4wXuPgNeGHfwgZ3bytlRJUrrNXMXExa1C8eu/js3+WiYFSXPIiTeuQaxsih+suj0abDTFjJygNMA=="
>>> base64_encode($rsa->encrypt('admin'))
=> "DVAFTjZ1Ah6mpN699/PitPj0nLNLhz4zGux9uYLQHmANR/CYEt63+z0vE2xQI9oKE/V4K5a335wvJpz+70hMy6G30cKAwerZ8PudbZgnpGRaF0YlRwzZEQ+XqV5qdQXE6kb4plVZrYrpyHAiPTmO54V+UvCp2YPbNY3Qcr+vbIrn0CZJ7lMwgE9NCGWwiIJF6G6/z4zqc+UbBG9+WUtf16BU8CXhbeU4FOSeuxYr9xbGbjAvtUZXrXpaQPYwgCuWURTWcN2fxmm5fAZuU74rPVMW5slTxTVafcIcUI0bFuMdsx3xj9VIImYhmbM0DTXT/gm66nxSy7aAvj5ckmemfA=="
>>> base64_encode($rsa->encrypt('admin'))
=> "Po8lzluh+ChWdHBxvThrn06kZ1cugTvEvV6UU8JroeM2aYwX4XH9hMSt1U+XmKkmdgxLRXwKUMsYcT7rF7I92tLW6T6fjTED1HoXXWS35okc1zhPTvXFqvMdXq7r4meLrcPmJjfsJsKnso4Ws2aIzHjQxfbPAHQNE2FD1bbOA0aD21MU8HR1qckTFosOP3O8KpxBQW3Z0aTX+k0sy0edwHfHCjQxQ9ne3oWcY+JiKCSFbyWoYxGQML0N11jA5Jik4K3jMA7cDQQzUutWUGN/ABv7OoXYKbfv/3IA8Uqu6jxSgBZjsLb4iUdFtw9QlwOsr4w2flCsPtye0vnDsjOduQ=="
>>> base64_encode($rsa->encrypt('admin'))
=> "rnkB57WUXMw87Hxc6e6MGraGryEQ+3HVwbGEexIGrs4jlqzBHZig7/ykfHzGgZGlrtYGY/FB3Mn6kfkwH1SXVAV6QPdnp0ktxbrqHJmQDDrLxsBlQGbZnSSVglV2EHn9Vm++iiygspv+IRMmOB6XOBxWMcV2AeAGa86EwTj3AKdcOeSPzrIB04G2mH695rEwLHC69KeBZ5vCuPAvVZ5AjhzsutcNEK5WLIcxcFbi1PQb7Amp3mPMpW3g0w+LkU6RkW8GGZ5gPu15PfvT/r8CCDaJbTwCLN2XPAa1R+/x0IZgSM2Tv/qOJBGdkkjGM8/lXeTGW8/oKU85bdlVqEBxdA=="
>>> base64_encode($rsa->encrypt('admin'))
=> "cX+CkQR+t9fdWMo2jHs7FdbmFzcAYkLfhNo6J0O3rAMtg8iR/KoN+RfNA6WQN4DIWMiGsN1F/ipAxiOo8K0V7x7EODgpbw2zGQBL/ueWJyD00UIA0WsXo93ubIXOJ+62dFbz7Ioc9Gtwv3q7HhtmKIh2oDxpffU1uOWzBGE8MW5cYvuWGHRqsgcKxFfiOJht+GkZUS2gdUe7/ke5YsEGLJs4PJzuk/NnPvsKEdlNHoAT+Vyzc3yW9+FtL8OpXDplACFaRS/Urulmvkd5wiy7dhCh9L1QDdgUZshkQ0In2254LFME5tVkpES0ZlD8eLZih84sioTIyeGoUj1gdkHxdw=="
>>> base64_encode($rsa->encrypt('admin'))
=> "JFVZ2dhi6vsLeiXwPDxisxRa+56fCHIM9RORhJ51hgdAr7qeb7O2o0Xx3AVJB/CfX6ZXOFwZ38sATGDpHgEbYT5Tui12IMMLTgLnQnnxyxX8+A5AfYCpAzHOr9Xr41fHdtVDTexcI+77yEakhh57SGhfUQhoTll/k73CDymLkF/DkKw21EY9DXgscffKB5giyC3bd4CiqCZ3j/aa0T70NKtjDT3H5zqrHd3dhqblYRVE7rAbKHYPz6hrv5TUX2rX45er4o53cihGOuzTlwOGDa5f0HGlepnXSvGlgmqCRMbina4LepAlker93HVD56I7rtiRRLqz9BUahkt3GSoZfA=="
>>>
When encryption is randomized, it provides semantic security. This property doesn't allow a passive observer of ciphertext to determine whether the plaintext behind the observed ciphertext was sent before or not. This is achieved through randomized padding (PKCS#1 v1.5 padding type 2 or OAEP).
If you want to determine whether your code is compatible, you need to encrypt on one side and decrypt on the other.
It looks like your code should work as-is, depending on what the defaults are. You should always specify your own configuration to prevent the defaults from breaking compatibility when you switch systems.
In Java, you should always provide a fully qualified cipher string:
Cipher.getInstance("RSA/ECB/PKCS1Padding"); or
Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); (OAEP is preferred nowadays)
Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); (OAEP is preferred nowadays)
In phpseclib, you should always change the defaults:
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1); or
for OAEP with SHA-1:
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_OAEP);
$rsa->setHash("sha1");
$rsa->setMGFHash("sha1");
for OAEP with SHA-256:
$rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_OAEP);
$rsa->setHash("sha256");
$rsa->setMGFHash("sha1");