I am trying to sign a string with the secret key I have, but it keeps failing. I am new to this and are just starting to understand it a little bit (encryption and base64). But I have been stuck at this point for three days now and have pulled out all of my hairs. I just dont get it.
The bit from the api is:
MD5 Digest
MD5 algorithm is used to perform a signature operation on the final string to be signed, thereby obtaining a signature hex result string (this string is assigned to the parameter sign). In the MD5 calculation result, letters are all capitalized.
RSA Signature
Users could use their private key to perform a signature operation (Base64 coded) to a MD5 digest by implementing SHA256 algorithm through RSA, after users getting the signature result string which is signed by MD5 algorithm.
All I have tried:
$echostr="SS...AD";
$api_key="17...e0";
$secret="EB....D";
$parameters="api_key=".$api_key."&echostr=".$echostr."&signature_method=HmacSHA256×tamp=".$timeStamp;
$preparedStr=base64_encode(strtoupper(md5($parameters)));
$sign=rsa_hash_sign($preparedStr, $secret);
echo "1 ".$sign."<\br>";
$sign=HmacSHA256_Sign($preparedStr, $secret);
echo "2 ".$sign."<\br>";
$sign=GenerateDigest($secret);
echo "3 ".$sign."<\br>";
$binaryKey = decode($secret);
$sign=encode(hash_hmac("sha256", $preparedStr, $binaryKey, true));
echo "4 ".$sign."<\br>";
$sign=hash_hmac("sha256", $preparedStr, $binaryKey, true);
echo "5 ".$sign."<\br>";
$sign=base64_encode(hash_hmac('sha256', base64_encode($preparedStr), $secret,true));
echo "6 ".$sign."<br>";
$cURLConnection = curl_init('...');
curl_setopt($cURLConnection, CURLOPT_POSTFIELDS, array(
'sign' => $sign,
'api_key' => $api_key
));
curl_setopt($cURLConnection, CURLOPT_HTTPHEADER, array(
'contentType: application/x-www-form-urlencoded',
'timestamp: '.$timeStamp,
'signature_method: HmacSHA256',//RSA
'echostr: '.$echostr
));
curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);
$apiResponse = curl_exec($cURLConnection);
curl_close($cURLConnection);
var_dump($apiResponse);
function encode($data) {
return str_replace(['+', '/'], ['-', '_'], base64_encode($data));
}
function decode($data) {
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
}
function GenerateDigest($requestPayload)
{
$utf8EncodedString = utf8_encode($requestPayload);
$digestEncode = hash("sha256", $utf8EncodedString, true);
return base64_encode($digestEncode);
}
function rsa_hash_sign($package, $privateKey) {
$rsa = new Crypt_RSA();
$rsa->setHash("sha256");
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
$rsa->loadKey($privateKey);
$hash = hash('sha256', $package, true);
$signature = $rsa->sign($hash);
$hexData = bin2hex($signature);
$base64 = base64_encode($hexData);
return $base64;
}
function HmacSHA256_Sign($preparedStr, $secret)
{
signature result string which is signed by MD5 algorithm.
$hash = "";
$rsa = new Crypt_RSA();
$rsa->setHash('sha256');
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
$rsa->loadKey($secret);
return $rsa->encrypt($secret);
}
But every $sign I try is giving me "Secret key does not exist" back as error. This is my first time trying anything with encryption and I am so lost and dont know how to solve this.
Related
I'm close but jwt.io doesn't like the signature I generate. With this code I generate the following JWT. If this ain't the way, how should I be generating a JWT in PHP if I can't use external libraries?
function gen_jwt():String{
$signing_key = "changeme";
// header always eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9
$header = [
"alg" => "HS512",
"typ" => "JWT"
];
$header = base64_url_encode(json_encode($header));
log_message('debug',__CLASS__.'('.__FUNCTION__.':'.__LINE__.') json base64_url_encode: ' . $header);
// test case 0 generates (on jwt.io):
// with secret base64 encoded: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjB9.ZW0gOCJV4e1KgGEsw0bL7oCF1AI1PBL8VVgSoss4tmr7682p6DpNc1uGbBpOEfkPjKJv0JBnLvjH2XUbo8PHUg
// without secret b64 encoded: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjB9.pqzfdCTmr-eWW9sEgV-COCdS4dI7MDpCIFWss6kXnAC9eLdGX1qOOr8BtJih59o_U_AdHtBh8JwUQ4dEPTk0rg
$payload = [
// "exp" => time() + ...,
"exp" => 0,
];
$payload = base64_url_encode(json_encode($payload));
$signature = hash_hmac('sha512', "$header.$payload", $signing_key, false);
log_message('debug',__CLASS__.'('.__FUNCTION__.':'.__LINE__.') signature: ' . $signature);
$signature = base64_url_encode($signature);
log_message('debug',__CLASS__.'('.__FUNCTION__.':'.__LINE__.') signature: ' . $signature);
// all three parts b64 url-encoded
$jwt = "$header.$payload.$signature";
log_message('debug',__CLASS__.'('.__FUNCTION__.':'.__LINE__.') jwt: ' . $jwt);
return $jwt;
}
/**
* per https://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid/15875555#15875555
*/
function base64_url_encode($text):String{
return str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode($text)
);
}
/**
* per https://www.uuidgenerator.net/dev-corner/php
*/
function guidv4($data = null): String {
// Generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = $data ?? random_bytes(16);
assert(strlen($data) == 16);
// Set version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
// Output the 36 character UUID.
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
Comes out:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjB9.ZmIxNzIyN2Q2ZjFhYjg3ZTJjMTY0NDJkNGQ4NzFlYWFmMjFhYzg1NzI5NGRkOGVhZmY4MTYzNWI1YTMyYWEyN2UxOTFmN2E5MzA1ZTZjZmI0OGVlZmMwN2U2NTc1MzNhZDg0NmMxMTZhZDZlOGVlYjJmMGVmOWUxOTMyYzE5MmE
...which jwt.io (and my own decoding efforts) say is an invalid signature. Help? Thanks!
I guess the trick was to base64-url-encode the binary output of the hmac like...
$signature = base64_url_encode(hash_hmac('sha512', "$header.$payload", $signing_key, true));
So the copy-paste-able code would be:
function gen_jwt():String{
$signing_key = "changeme";
$header = [
"alg" => "HS512",
"typ" => "JWT"
];
$header = base64_url_encode(json_encode($header));
$payload = [
"exp" => 0,
];
$payload = base64_url_encode(json_encode($payload));
$signature = base64_url_encode(hash_hmac('sha512', "$header.$payload", $signing_key, true));
$jwt = "$header.$payload.$signature";
return $jwt;
}
/**
* per https://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid/15875555#15875555
*/
function base64_url_encode($text):String{
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($text));
}
I have the following decrypted message, which has previously been encrypted using AES-256-CBC
240dcbefc0f82fadc00ef8494488aaa81400000c2def01e79fec6c4d9a822358dd8a910cac606e8afcb607793cb442093a56b7b40b0b0b0b0b0b0b0b0b0b0b0b
I derive the following 20 BYTE HMAC from this message:
dd8a910cac606e8afcb607793cb442093a56b7b4
My goal is to re-create this HMAC using PHP, I attempt with the following code:
$iv = hex2bin('240dcbefc0f82fadc00ef8494488aaa8'); // random iv - first 16 bytes of the message
$message = hex2bin('1400000c2def01e79fec6c4d9a822358'); // the actual message being decrypted - next 16 bytes
$key = hex2bin('b109124b62e2c8b8248e9865990325fddcc61143'); // encryption key
$hmac = hash_hmac('sha1', $iv.$message, $key);
print($hmac); // 03634ba3f4a0c854a0b791d27f331ecdfad1e87e
$attempt2 = hash('sha256', $iv.$message, true);
$hmac = hash_hmac('sha1', $attempt2, $key);
print($hmac); // 39ad1fb94ab251cdaf3f21cf8673e070733f4e16
I know I'm missing something but I'm struggling to understand the HMAC process as it's very confusing to me. Any help or advise is appreciated, thanks.
I found a solution on a random blog post online :D
here is what worked for me:
function check_mac($seq, $type, $msg, $key) {
$SequenceNumber = pack("NN", 0, $seq);
$Type = pack("Cnn", $type, 0x0303, strlen($msg));
$data = $SequenceNumber . $Type . $msg;
$calculated_mac = hash_hmac("sha1", $data, $key, true);
print(bin2hex($calculated_mac) . "\n");
}
Also, the IV does not need to be included, just the message by itself as the $msg variable
Blog Post:
https://adayinthelifeof.nl/2013/12/30/decoding-tls-with-php/
I have now spent the last couple of days to find documentation about this..
I need to send a XML via SOAP with the WSSE security header, but don't know how to encrypt and store the encrypted keys
Here is an example
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" soap:mustUnderstand="1">
<xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EK-1B758D26C51BFCD86614340101135741">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference>
<wsse:KeyIdentifier EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3">MIIDODCCAiCgAwIBAgIGAU0FlCVCMA0GCSqGSIb3DQEBCwUAMFoxCzAJBgNVBAYTAkRLMRUwEwYDVQQKEwxCYW5rIENvbm5lY3QxFTATBgNVBAsTDEJhbmsgQ29ubmVjdDEdMBsGA1UEAxMUQmFuayBDb25uZWN0IElBLXRlc3QwHhcNMTUwNDI5MTQyODI0WhcNMTgwNDI5MTQyODI0WjAcMRowGAYDVQQDExFiYW5rIGNvbm5lY3QtdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI23KdtaRKPTFTe/A1PnsF9dpSlTiXurKmio0OCgTP9wClHwync3JsInRwGTooA20P9zWobUnEFbEiAgRVYCxuYoldRE6NLhSC854/YTjMBeevH1TNa38lpavGiI4UwFhg70U9/JuYs21hoFyzVfaWlVfOkAMm1U/n4wHq6FZW461S5PY4A/UI1Mr8WgeIHU9GqMBtFvjynzq3SLenOPgdmKtyJ3V8EOU+DlgwKmDbxMVMtYNDZtoQvOWnuvlJ6ICDcqcW7OUkmwCKodjxxPvrdaPxyZDhT7h4FgRtrAOS8qR6L7x9D4ZIoxOMPudGvr99OSb4KVtaAEt/R7hKxG3OsCAwEAAaNCMEAwHwYDVR0jBBgwFoAU680YSkZnx1IaJAmI49LlTGiia0wwHQYDVR0OBBYEFMaWOY7Vf/iB3WVA96j5kRtbF8prMA0GCSqGSIb3DQEBCwUAA4IBAQAJ+bssSFWE6KsYT7HSDKag4Eot7yNGMY4Don/MilDnOREdu20QUS131DKrSkpBQiCXbyRUQjUoun4yue0EG+rlG3QUIlNNdJ4KZJB+dTYdLUV7XTYJNPimKAmoZ+PFNvT1eGgWcMT+MbTfpk0mw0V8IprYGa8UPchd6vtSVwpbTcPc/F4bgUTlm/V+FG4bQS61gF0koj0DEZjzat7CBHpozRgfRlXgwu26vnhWGc99uKH4GAKN4JpqPi/6Yz+7iQNJUC3yeezgBxFrIXuLpkBZSP4zunf9VxsICnxkFUXOTuYBdcbhPNzqMknD5ijFcFRZITwdv7x3uJGLkM7iUfBp</wsse:KeyIdentifier>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>af9+FhA91ytLwjeRvTYJsRCkhjHmAQGwqYwMBoNZBn7BZhF/a6EUpM9ByarVhx1SRCpjW5fb8tBVuJO1ZkjfTUZ5EAh/oDLbkmwPdSAAVzmAURHwCq3XQgMZV3lAczlLnPamxjjZBCGqxvAmBo1CvFFPC4AcBedqY92mP8XGyVHpS7JYKOxqXK2vUA1by7371x+Mu0aoS2zJPyPLa1IPwOYgR9qicmWz1RNPiEVA8ZBCN0NRyg7FLJxdUcE81z+1SjButBo2j3qcwkNcecHzZAnweY+LSWp3H5JA3WNzUHUuvFHEaPzT5jd7fUI16xo8NLK8/Rd8Eq/zDD+T3baeVQ==</xenc:CipherValue>
</xenc:CipherData>
<xenc:ReferenceList>
<xenc:DataReference URI="#ED-1B758D26C51BFCD86614340101135852"/>
</xenc:ReferenceList>
</xenc:EncryptedKey>
</wsse:Security>
<technicalAddress xmlns="http://example.com/schema/2014" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#"/>
<activationHeader xmlns="http://example.com/schema/2014" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#">
<organisationIdentification>
<mainRegistrationNumber>8079</mainRegistrationNumber>
<isoCountryCode>DK</isoCountryCode>
</organisationIdentification>
<functionIdentification>112233445566778899</functionIdentification>
<erpInformation/>
<endToEndMessageId>d28b6a7dad414014a59029ef1a7e84d4</endToEndMessageId>
<createDateTime>2015-06-11T10:08:33.258+02:00</createDateTime>
</activationHeader>
</soap:Header>
<soap:Body>
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="ED-1B758D26C51BFCD86614340101135852" Type="http://www.w3.org/2001/04/xmlenc#Content">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsse11="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" wsse11:TokenType="http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKey">
<wsse:Reference URI="#EK-1B758D26C51BFCD86614340101135741"/>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>dTSVuEJ90OYguQOsOz2ZtcE2mybwuvVl19pp7/e5yuvNygx3w5v+prpEvbjYLauiIAB3lrVDK2astJeYJGnDbaVJVeU0YqH5ItYVn7Wz36jJM52KB+UNbYo8EdTKYjsZuADzH+tAoA+pwYxGBXMEQctNI+C711HgP2hbpHNYOG7nAMOIrP/0B3FCy+st+9CbYlwAEENreTYunEEA41hciFnWCsIx0el7OeuiA6V51fAmvrF19RPNKwaptvbvmVdKj//RQ/0U1kRny16mDnFfX92bI3HBQm4XJA0nEfSvio7EUAAdhe77GMfu7+JELqXNowPGPLlvrbCFYnQhxGRITHtTIEbtJA6MKtBzHgjtw5pt7oWxKgGUnaJTfOPOSv43RLFGggkT/+gTjnZOagu8hhXp0x5HXJuZzw90aIS3jAfSPDc2ivct4WhWk0wcuQyC2rAh4I7gtiR+LqJJGqvucw4S+NR95FunKHKEW4yasKW1oU31/rRbp4Bmwo6BPsQlxnaSHPtk68IVkYDBslz1A5gOP+M/Iam2WI02y6sE/7aAH1ruN3pZlVuYFc3JDNHOPOvevP110d60lroknGdc9vxcFfj48OCKw/8Ed6tiXtAvk0Qu9Qt4ZyLUoPKIWEqjdLjwVadTDJQFAxRptNgiCos7s0czadUu7FNCRxfndjDxhA7trvys44ufEyK++YzZIgNu3r4dywNI22Nm+JZtLj+rX8ARE6FTPlxGBD0SSdXsfCfY2N1ytBBHQRnPsVaHK1p7KOhwQVbqEupcGyvaRolnymOzDLGFdS06OGYFrYXdgIbuqYtZP8QerXtUl0sWNAvvqHSPCQcpKecpMEecar+FUVwLEA+H1wzOprCMbRR+EgIboeDqQ7GxXqugkuFyvnlLDgxnaWhEhQb/5kAcQmnyUZ57MhDcUJqqQ4Cdmwrcxho1P+YqWY9yn0E86F+hl5976a/gH5KBobB84OWmgcX42eAmqpJf+8c8SuBv+7NctbQOk21aYlFEpkwSme/kG1/edtyoHQH/hF0RB1cT8g+u9S9AK2rs3s2G+Ap0U5oyY8pqJalGdZSBudE0sU4mhOV8trtx0FrN9A7pNkTcGPH25nCtyIz6rzR+DP8Mtgw5385s5ivVlDb+z74Wbh6iu7ZkVAogNTpUYU/1BxDXWJqFMkFmfziNxQ5AQqm1vGlBzXifoQkUFX1riutNphmu0Hs+7KMmMLvtW2cXmQDpkHFKVheeN4w7pBCEZ8KhZ0VTOwRZcdvrNcpYfXM13/QdTHQmCqqwgS/VvlUFz7PDn0/OKo6moUic8W6b1iEvd3kfc7QkunxoOUoJr4RwJ+PqCzN6PxQivAFA2tmDPc8qEa1PAdxTeNFoR/6dNQRojouuJq3C1LrbmGf6lQPvKi3KeKHXyjmDr7Tve+al2tcWJVr+1qEM3/XuthoiZbuTDxYUjZ2nf2fhHrmNcfvrfNxSNHVdQPp2R9Rf3eGxlRJsmRpef66VbYhOpmiH4xmq45EWiyBZmYm+tZtjsP51EDMIvdFbVRSGO/hMqURrDSsJXJeot27Iup2s0P2n/6a9k0c4SVvf/WXNN5x9JNvjU97bQNDQRfonJmo9pRYYHl1tSqNIYBK7KsMH+qr1vmiJuhrXUuL/RtOKvE9KXQ8kGoC9oF5rFn21z40ElxG5XRTASg==</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</soap:Body>
</soap:Envelope>
First of all I have never worked with SOAP before so chances I do things wrong has pretty good odds :)
Have found something here, but I need more details https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html#aes256-cbc
How are the iv and the key stored in CipherValue in the header?
When sending the XML request to the webservice I get this error
23-08-2018 12:50:02 General exception:Padding is invalid and cannot be removed.
23-08-2018 12:50:02 Stack trace: at System.Security.Cryptography.CapiSymmetricAlgorithm.DepadBlock(Byte[] block, Int32 offset, Int32 count)
at System.Security.Cryptography.CapiSymmetricAlgorithm.TransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)
at System.Security.Cryptography.Xml.EncryptedXml.DecryptData(EncryptedData encryptedData, SymmetricAlgorithm symmetricAlgorithm)
at SomeClassCore.XmlSecurity.Decryptor.DecryptData(Byte[] symmetricKey)
at SomeClassCore.SecurityServiceImpl.UnwrapRequest(ServiceRequest serviceRequest)
at BD.BCA.MessageHandler.MessageHandler.ProcessRequest(HttpContext context)
Have searched a bit more.. Maybe the iv must be a part of the stored data. But it's still not working? Same error as above
class Encryption {
const AES256_CBC = 'AES-256-CBC';
public function data_encrypt(string $data, string $cipher): Array{
switch($cipher){
case self::AES256_CBC:
$key_length = 32;
$block_length = 16;
break;
}
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
$key = openssl_random_pseudo_bytes($key_length);
$encrypted_data = $iv.openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv);
return [
'data' => base64_encode($this->pkcs7_padding($encrypted_data, $block_length)),
'key' => $key
];
}
public function key_encrypt(string $key): string{
$public_cert = openssl_pkey_get_public('contents of public cert');
openssl_public_encrypt($key, $data, $public_cert, OPENSSL_PKCS1_OAEP_PADDING);
openssl_free_key($public_cert);
return base64_encode($data);
}
private function pkcs7_padding(string $data, int $block_length): string{
$pad = $block_length - (strlen($data) % $block_length);
return $data.str_repeat(chr($pad), $pad);
}
}
$Enc = new Encryption;
$data_encrypted = $Enc->data_encrypt('The message I want to encrypt', Encryption::AES256_CBC);
// This base64 encoded string goes to <EncryptedData>
$data_encrypted['data'];
// This base64 encoded string goes to <EncryptedKey> in the header
$Enc->key_encrypt($data_encrypted['key']);
update
Have been in contact with the maintainer of the webservice and OAEP padding is used with the RSA encryption and PKCS7 padding is used with AES chipher..
As I can see this is also what I do?
*TESTED AND WORKING CODE *
I would suggest that you separate the different parts involved. The most likely cause to your problems is the order of execution (i.e. you should do padding before encryption). I am also surprised that there is no signature, but that might not be required in your case. However, I prepared the suggested code for you to test and also added decrypt/decode functions to make testing easier. Good luck.
<?php
class Encryption {
const AES256_CBC = 'AES-256-CBC';
const IV_BYTES = 16;
protected $binary_security_token = null;
protected $private_key = null;
protected $public_key = null;
public function data_encrypt(string $data, string $password): Array {
$key = hash('sha256', $password, true);
$iv = openssl_random_pseudo_bytes(self::IV_BYTES);
$padding = 16 - (strlen($data) % 16);
$data .= str_repeat(chr($padding), $padding);
$encrypted_data = openssl_encrypt($data, self::AES256_CBC, $key, OPENSSL_RAW_DATA, $iv);
$encoded_data = base64_encode($iv . $encrypted_data);
return [
'data' => $encoded_data,
'key' => $key
];
}
public function data_decrypt(string $data, string $password): Array {
$decoded_data = base64_decode($data);
$key = hash('sha256', $password, true);
$iv = substr($decoded_data, 0, self::IV_BYTES);
$encrypted_data = substr($decoded_data, self::IV_BYTES);
$decrypted_data = openssl_decrypt($encrypted_data, self::AES256_CBC, $key, OPENSSL_RAW_DATA, $iv);
$padding = ord($decrypted_data[strlen($decrypted_data) - 1]);
return [
'data' => substr($decrypted_data, 0, -$padding)
];
}
public function key_encrypt(string $key): ?string {
$encoded_data = null;
if ($this->public_key && openssl_public_encrypt($key, $data, $this->public_key, OPENSSL_PKCS1_OAEP_PADDING)) {
$encoded_data = base64_encode($data);
}
// openssl_free_key($this->public_key);
return $encoded_data;
}
public function key_decrypt(string $data): ?string {
$decrypted_data = null;
$decoded_data = base64_decode($data, true);
if ($this->private_key && openssl_private_decrypt($decoded_data, $decrypted, $this->private_key, OPENSSL_PKCS1_OAEP_PADDING)) {
$decrypted_data = $decrypted;
}
// openssl_free_key($decrypted);
return $decrypted_data;
}
public function generate_keys(): void {
$config = [ "private_key_bits" => 2048, "private_key_type" => OPENSSL_KEYTYPE_RSA ];
$resource = openssl_pkey_new($config);
if (openssl_pkey_export($resource, $this->private_key)) {
echo "private_key:\n" . $this->private_key . "\n";
$private_key_file = "private_key.pem";
file_put_contents("private_key.pem" , $this->private_key);
}
$this->public_key = openssl_pkey_get_details($resource);
$this->public_key = $this->public_key["key"];
$this->binary_security_token = preg_replace("#-.+-|[\r\n]| #", "", $this->public_key);
echo "public_key:\n" . $this->public_key . "\n";
file_put_contents("public_key.pem", $this->public_key);
}
public function load_keys(): void {
$private_key_path = realpath(dirname(__FILE__) . "/private_key.pem");
if (!$private_key_path) {
$this->generate_keys();
return;
}
$private_key_contents = file_get_contents($private_key_path);
if (!$private_key_contents) {
$this->generate_keys();
return;
}
$public_key_path = realpath(dirname(__FILE__) . "/public_key.pem");
if (!$public_key_path) {
$this->generate_keys();
return;
}
$public_key_contents = file_get_contents($public_key_path);
if (!$public_key_contents) {
$this->generate_keys();
return;
}
// Signature to see that data is not manipulated, could be performed on an encrypted body. The spec says you only make a signature for what you can see.
// Is it important to "hide data", "detect manipulated data" or both ...
$this->binary_security_token = preg_replace("#-.+-|[\r\n]| #", "", $public_key_contents); // BinarySecurityToken for securityToken in Security header
// ValueType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
// EncodingType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
if (openssl_pkey_export($private_key_contents, $this->private_key)) {
echo "private_key:\n" . $this->private_key . "\n";
}
$public_resource = openssl_pkey_get_public($public_key_contents);
if ($public_resource) {
$this->public_key = openssl_pkey_get_details($public_resource);
$this->public_key = $this->public_key["key"];
echo "public_key:\n" . $this->public_key . "\n";
}
}
}
$enc = new Encryption();
$encrypted = $enc->data_encrypt("The message I want to encrypt", "password");
// This base64 encoded string goes to <EncryptedData>
// $encrypted['data']
// Test that data_encrypt / data_decrypt works (from a terminal)
echo "encrypted data:\n" . $encrypted["data"] . "\n";
$decrypted = $enc->data_decrypt($encrypted["data"], "password");
echo "decrypted data:\n" . $decrypted["data"] . "\n";
// This base64 encoded string goes to <EncryptedKey> in the header
// $enc->key_encrypt($encrypted['key']);
if (version_compare(phpversion(), "7.1.0", ">=")) {
$enc->load_keys();
$pwd_hash_pre = bin2hex($encrypted["key"]);
echo "hex key:" . $pwd_hash_pre . "\n";
$encrypted_key = $enc->key_encrypt($encrypted["key"]);
echo "\nencrypted and base64encoded key:" . $encrypted_key . "\n";
$decrypted_key = $enc->key_decrypt($encrypted_key);
$pwd_hash_post = bin2hex($decrypted_key);
echo "\ndecrypted and decoded key:" . $pwd_hash_post . "\n";
$equal_hashes = $pwd_hash_pre === $pwd_hash_post ? 'true' : 'false';
echo "password hashes equal:" . $equal_hashes . "\n";
}
Your error suggest that the API failed to decrypt the openssl AES-256-CBC data.
I think the reason is because in your class you are routing the encryption through your pkcs7_padding() function. I believe that by default, as long as you don't specify OPENSSL_ZERO_PADDING in your openssl_encrypt() function that the padding is pkcs7. The block size for all AES encryption is 128bits or 16 bytes.
So in essence you are padding your already padded encryption. So basically I just removed your pkcs7_padding() from your class.
I did test your public key encryption. I was able to use a 2048b rsa key from 2048b certificate and generate an encrypted public key using a PEM formatted certificate. Whether or not it's padded correctly I have no idea. But the OPENSSL_PKCS1_OAEP_PADDING is probably correct.
My guess is that the RSA encryption worked if the API made it to the AES portion.
As far as how you are assembling the data into the XML I don't have a clue.
But is seems reasonable that in the <xenc:EncryptedKey> tag in the cipher value would be the RSA encrypted key and for the <xenc:EncryptedData> tag the cipher value would be the AES ecrypted data. You just have to figure out how the API is getting the IV.
Read your API docs for how they are expecting the IV to be delivered. I will keep looking too if this does not work for you.
I will research on that later. But for know try without manually padding your encryption. Hope it helps.
Another thing to consider is that in your case example you don't need to use an IV. In your AES encryption you are generating a new key for every encryption and then encrypting the AES key via the RSA public key obtained by your certificate.
If you were using the same AES key I would see a need to implement an IV but in this case I don't. Regardless... We need to know if the API expects an IV and if it does, how it is expected to be sent?
class Encryption {
const AES256_CBC = 'AES-256-CBC';
public function __construct(){
}
public function data_encrypt($data, $cipher){
switch($cipher){
case self::AES256_CBC:
$key_length = 32;
$block_length = 16;
break;
}
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
$key = openssl_random_pseudo_bytes($key_length);
$encrypted_data = $iv . openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv);
return [
'data' => base64_encode($encrypted_data),
'key' => $key //Does this need to be encoded?
];
}
//***Important***
//Make sure you certificate is 1.) a x.509 certificate resource, 2.)A file path that leads to a PEM encoded certificate, or 3.) a PEM formatted key.
public function key_encrypt($text){
$keyResource = openssl_pkey_get_public(file_get_contents('path/to/myCert.pem')); //This returns a resource or FALSE.
if(!$keyResource){
echo 'Something wrong with certificate.';
}
openssl_public_encrypt($text, $cipherText, $keyResource, OPENSSL_PKCS1_OAEP_PADDING);
openssl_free_key($keyResource);
return base64_encode($cipherText);
}
}
$Enc = new Encryption;
$cipherText = $Enc->data_encrypt('The message I want to encrypt', Encryption::AES256_CBC);
// This base64 encoded string goes to <EncryptedData>
echo 'AES Data: ' . $cipherText['data'] . '<br><br>';
echo 'AES Key: ' . $cipherText['key'] . '<br><br>';
// This base64 encoded string goes to <EncryptedKey> in the header
$key = $Enc->key_encrypt($cipherText['key']);
echo 'RSA OAEP Padded Key: ' . $key;
I'm trying to setup an app to sign with my URLs so they may authenticate but I can't seem to figure out how to replicate the code that I'm trying from the following page: https://help.sendowl.com/help/signed-urls#example
order_id=12345&buyer_name=Test+Man&buyer_email=test%40test.com&product_id=123&signature=QpIEZjEmEMZV%2FHYtinoOj5bqAFw%3D
buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123
buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t
publicStr&t0ps3cr3t
This is the steps:
First order the parameters (removing the signature) and unescape
them:
Next append your Signing secret:
Generate the key to sign with:
Perform the HMAC-SHA1 digest with Base 64 encode: QpIEZjEmEMZV/HYtinoOj5bqAFw=
The following is what I tried but end up not getting the same result:
$signKey = "t0ps3cr3t";
$signData = "buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t";
$passData = hash_hmac("sha1", $signData, base64_decode(strtr($signKey)), true);
$passData = base64_encode($passData);
echo $passData;
I keep getting x8NXmAmkNBPYCXwtj65mdVJ8lPc=
I was able to replicate with the following: took me a bit to figure out something so simple.. been coding for 11 hours straight.
Thanks.
$data = "buyer_email=test#test.com&buyer_name=Test Man&order_id=12345&product_id=123&secret=t0ps3cr3t";
$key = "publicStr&t0ps3cr3t";
$pass1 = hash_hmac('sha1', $data, $key, true);
$pass = base64_encode($pass1);
echo $pass;
$pass will return "QpIEZjEmEMZV/HYtinoOj5bqAFw=", the correct value.
$current_timestamp = Carbon::now()->timestamp;
$signstring = "z001-line-anime-gif." . $current_timestamp . ".aaaabbbbccccdddd";
$secret = 'STG-7f*(:hsM-1_eQ_ZD175QgEoJhI$:oR.zEQ<z';
$sig = hash_hmac('sha1', $signstring, $secret);
$signature = hex2bin($sig);
$signature = base64_encode($signature);
return $signature;
I am trying to utilize the GoodRx API using PHP.
Here is my code:
$hash = hash_hmac('sha256', $query_string, MY_SECRET_KEY);
$encoded = base64_encode($hash);
$private_key = str_replace('+', '_', $encoded);
$private_key = str_replace('/', '_', $encoded);
//$private_key = urlencode($private_key);
$query_string .= '&sig=' . $private_key;
echo $query_string;
// https://api.goodrx.com/low-price?name=Lipitor&api_key=MY_API_KEY&sig=MY_SECRET_KEY
It is returning an error saying that my sig is not right.
Could you help me please.
Thank you.
Thomas.
Posting this in case anyone needs a complete example of how to make a basic call to the GoodRx API:
In Python:
import requests
import hashlib
import hmac
import base64
# python --version returns:
# Python 3.5.1 :: Anaconda 2.4.1 (32-bit)
# my_api_key is the API key for your account, provided by GoodRx
my_api_key = "YOUR_API_KEY";
# s_skey is the secret key for your account, provided by GoodRx
my_secret_key=str.encode("YOUR_SECRET_KEY", 'utf-8')
# Create the base URL and the URL parameters
# The url_params start as text and then have to be encoded into bytes
url_base = "https://api.goodrx.com/fair-price?"
url_params = str.encode("name=lipitor&api_key=" + my_api_key, 'utf-8')
# This does the hash of url_params with my_secret_key
tmpsig = hmac.new(my_secret_key, msg=url_params, digestmod=hashlib.sha256).digest()
# Base64 encoding gets rid of characters that can't go in to URLs.
# GoodRx specifically asks for these to be replaced with "_"
sig = base64.b64encode(tmpsig, str.encode("__", 'utf-8') )
# Convert the sig back to ascii
z = sig.decode('ascii')
# add the sig to the URL base
url_base += url_params.decode('ascii') + "&sig=" + z
# request the URL base
r = requests.get(url_base)
# print the response
print(r.content)
You should get something like this for the output:
{"errors": [], "data": {"mobile_url":
"http://m.goodrx.com/?grx_ref=api#/drug/atorvastatin/tablet", "form":
"tablet", "url": "http://www.goodrx.com/atorvastatin?grx_ref=api",
"brand": ["lipitor"], "dosage": "20mg", "price": 12.0, "generic":
["atorvastatin"], "quantity": 30, "display": "Lipitor (atorvastatin)",
"manufacturer": "generic"}, "success": true}
Here's similar code in PHP, looking up the drug Apidra Solostar, which has NDC 00088250205:
<?php
function base64url_encode($data) {
return strtr(base64_encode($data), '+/', '__');
}
$my_api_key = "YOUR_API_KEY";
$s_key="YOUR_SECRET_KEY";
$ndc = "00088250205";
// Initialize the CURL package. This is the thing that sends HTTP requests
$ch = curl_init();
// Create the URL and the hash
$url = "https://api.goodrx.com/fair-price?";
$query_string="ndc=" . $ndc . "&api_key=" . $my_api_key;
$tmp_sig = hash_hmac('sha256', $query_string, $s_key, true);
$sig = base64url_encode( $tmp_sig );
$url = $url . $query_string . "&sig=" . $sig;
// set some curl options
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_VERBOSE, true);
// run the query
$response = curl_exec($ch);
var_dump($response);
?>
Thanks to Thomas for talking with me on this.
You're not doing your string replacements correctly:
$private_key = str_replace('+', '_', $encoded);
^^---new string ^---original string
$private_key = str_replace('/', '_', $encoded);
^--overwrite previous replacement ^---with original string again
If you want to chain replacements, you have to do something more like:
$orig = 'foo';
$temp = str_replace(..., $orig);
$temp = str_replace(..., $temp);
...
$final = str_replace(..., $temp);
Note how you pass in the result of the PREVIOUS replacement into the next call, which is what you're not doing. You just keep taking the originals tring, replace one thing, then trash that replacement on the next call. So effectively you're ONLY doing a /->_ replacement, and sending the + as-is.