PayPal webhook signature verification in C# - c#

I have a C# application that receives webhook notifications from PayPal and I want to verify the signature as described in PayPal docs:
https://developer.paypal.com/docs/integration/direct/rest-webhooks-overview/#event-types
The code snippet in the docs is for Java, not C#. The first thing I don't know is in which format the CRC32 should be appended (decimal, hex, ???). I have tried several variants and I have following code so far, always with VerifyData() returning false:
string transmissionSig = HttpContext.Request.Headers["PAYPAL-TRANSMISSION-SIG"];
string transmissionId = HttpContext.Request.Headers["PAYPAL-TRANSMISSION-ID"];
string transmissionTime = HttpContext.Request.Headers["PAYPAL-TRANSMISSION-TIME"];
string signatureAlgorithm = HttpContext.Request.Headers["PAYPAL-AUTH-ALGO"]; //signatureAlgorithm == "SHA256withRSA"
string certUrl = HttpContext.Request.Headers["PAYPAL-CERT-URL"];
uint crc = calculateCrc32(eventBody);
string expectedSignature = String.Format("{0}|{1}|{2}|{3}", transmissionId, transmissionTime, webhookId, crc);
string certData = new System.Net.WebClient().DownloadString(certUrl);
X509Certificate2 cert = new X509Certificate2(getBytes(certData));
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)cert.PublicKey.Key;
byte[] signature = Convert.FromBase64String(transmissionSig);
byte[] expectedBytes = getBytes(expectedSignature);
bool verified = rsa.VerifyData(expectedBytes, CryptoConfig.MapNameToOID("SHA1"), signature);
What am I doing wrong?
UPDATE
I use this class for CRC calculation: https://github.com/damieng/DamienGKit/blob/master/CSharp/DamienG.Library/Security/Cryptography/Crc32.cs
Example eventBody (from webhook simulator):
{"id":"WH-2WR32451HC0233532-67976317FL4543714","create_time":"2014-10-23T17:23:52Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"A successful sale payment was made for $ 0.48 USD","resource":{"id":"80021663DE681814L","create_time":"2014-10-23T17:22:56Z","update_time":"2014-10-23T17:23:04Z","amount":{"total":"0.48","currency":"USD"},"payment_mode":"ECHECK","state":"completed","protection_eligibility":"ELIGIBLE","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","clearing_time":"2014-10-30T07:00:00Z","parent_payment":"PAY-1PA12106FU478450MKRETS4A","links":[{"href":"https://api.paypal.com/v1/payments/sale/80021663DE681814L","rel":"self","method":"GET"},{"href":"https://api.paypal.com/v1/payments/sale/80021663DE681814L/refund","rel":"refund","method":"POST"},{"href":"https://api.paypal.com/v1/payments/payment/PAY-1PA12106FU478450MKRETS4A","rel":"parent_payment","method":"GET"}]},"links":[{"href":"https://api.paypal.com/v1/notifications/webhooks-events/WH-2WR32451HC0233532-67976317FL4543714","rel":"self","method":"GET"},{"href":"https://api.paypal.com/v1/notifications/webhooks-events/WH-2WR32451HC0233532-67976317FL4543714/resend","rel":"resend","method":"POST"}]}
And it's CRC that I'm getting and appending to expectedSignature: 3561502039

you should get algorithm from the header in stead of hard-coding it. SHA256 is the currently supported algorithm I think.

Related

Why do I get an different md5 hash values between R and C# with the same message?

This C# code:
private static string CreateHashKey(object myString)
{
byte[] buffer = JsonSerializer.SerializeToUtf8Bytes(myString);
var cryptoTransform = MD5.Create();
return BitConverter.ToString(cryptoTransform.ComputeHash(buffer));
}
When given the string "Bangalore", produces this hashed value: "92-E7-92-78-E7-D9-37-C1-AF-AC-D7-E6-B2-CD-B6-5E".
However, I cannot reproduce this in R, playing around a bit:
library(digest)
digest::digest(serialize("Bangalore", connection = NULL),"md5")
"e85798ec7dd5003d8d464f6c5d8de5c5"
digest::digest(serialize("Bangalore", connection = NULL,ascii = TRUE),"md5")
"5377a11b9792c774ddd726361c56d8f2"
digest::digest(charToRaw('Bangalore'),"md5")
"bf19f50fed3db016cb78cdb029db3034"
digest::digest(serialize(charToRaw('Bangalore'),connection = NULL,ascii = TRUE),"md5")
"be7696c777f9418e8e853084d6ddf0ae"
library(openssl)
md5("Bangalore") # "1bc99cb2f4153c2d0d8025ee5575b2a0"
This post suggests converting to bytes would fix the problem, but it hasn't so far...
Why do I get an different hash when running with python and C# the same key and message?

SignedCms.CheckSignature with SAP certificate is failing

This is a follow-up of this question.
I am writing an external server which gets called by a SAP-Server. The SAP-Server signs the URL with a certificate before it is transmitted. In a previous step the SAP-Server sent the certificate it will be using to sign the URL to my server. So my server has the certificate the SAP-Server is using for signing.
From the SAP-documentation I know the following.
The unsigned URL looks like this
http://pswdf009:1080/ContentServer/ContentServer.dll?get&pVersion=0046&contRep=K1&docId=361A524A3ECB5459E0000800099245EC&accessMode=r&authId=pawdf054_BCE_26&expiration=19981104091537
The values of important QueryString-parameters are concatenated (in the same order they appear in the QueryString) to form the "message".
For the given QueryString-Parameters
ContRep = K1
DocId = 361A524A3ECB5459E0000800099245EC
AccessMode = r
AuthId = pawdf054_BCE_26
Expiration = 19981104091537
the generated "message" looks like this:
K1361A524A3ECB5459E0000800099245ECrpawdf054_BCE_2619981104091537
The "message" is used to calculate the hash from which the SecKey is calculated. SAP uses the Digital Signature Standard (DSS) to digitally sign the hash value according to PKCS#. The digital signature is appended to the querystring in a parameter with the name SecKey. The SecKey for the chosen procedure is about 500 bytes long. In the example from the SAP-documentation, the arbitary values 0x83, 0x70, 0x21, 0x42 are chosen for the secKey, for the sake of clarity.
The SecKey is base64 encoded and added to the URL.
0x83, 0x70, 0x21, 0x42 gets to "g3AhQg=="
and the transferred URL looks like this
http://pswdf009:1080/ContentServer/ContentServer.dll?get&pVersion=0046&contRep=K1&docId=361A524A3ECB5459E0000800099245EC&accessMode=r&authId=pawdf054_BCE_26&expiration=19981104091537&secKey=g3AhQg%3D%3D
When my server receives the URL I need to check the signature. I recreate the "message" by concatenating the QueryString-parameters the same way as it was described in point 2. (as it is described in the SAP-documentation)
SAP gives this Summary of Technical Information
Format of digital signature: PKCS#7 "signed data"
Public key procedure: DSS
Key length: 512 - 1024 bits
Public exponent: 2^16 + 1
Public key format: X.509 v3 certificate
MD (message digest) algorithm: MD5 or RIPEMD-160
The library for checking signatures can be obtained from SAP AG. Because the standard format PKCS#7 was used for the signature, other products can also be used for decoding.
I receive an "The hash value is not correct"-Exception on line cms.CheckSignature(certificates, true);
private void CheckSignature(string secKey, string message, X509Certificate2 cert)
{
byte[] signature = Convert.FromBase64String(secKey);
ContentInfo ci = new ContentInfo(System.Text.Encoding.ASCII.GetBytes(message));
SignedCms cms = new SignedCms(ci, true);
X509Certificate2Collection certificates = new X509Certificate2Collection(cert);
cms.Decode(signature);
try
{
cms.CheckSignature(certificates, true);
}
catch(Exception ex)
{
log.Error(ex.ToString());
}
}
Can anybody help, or knows what I am doing wrong?
Actually the above function CheckSignature works correct
BUT the second parameter 'message' has to be URL-encoded. Or to be more precise, when concatenating you must use the NOT-URL-DECODED queryString values. [with the same spelling (uppercase/lowercase) SAP uses]
ContRep = AA
DocId = 53730C7E18661EDCB1F816798DAA18B2
AccessMode = r
AuthId = CN=NPL (for concatenating 'CN%3DNPL' is used)
Expiration = 20220511173746
will become the message
AA53730C7E18661EDCB1F816798DAA18B2rCN%3DNPL20220511173746

RNCryptor in C# and Swift, RNCryptorError error 2

I am using RNCryptor in Swift and C#.NET . I need a cross platform AES encryption and because of this, I am using RNCryptor.
When I encrypt some plain text in Swift,I can decrypt it in Swift correctly without any error. But when I encrypt some text in C# and then I want to decrypt it in Swift,I got an error " The operation couldn’t be completed. (RNCryptorError error 2.)"
My code in C# :
public static string EncryptQRCode(string qrCodeString){
var qrEncryptor = new Encryptor ();
return qrEncryptor.Encrypt (qrCodeString, "password");
}
public static string DecryptQRCode(string qrEncryptedString){
var qrDecryptor = new Decryptor();
return qrDecryptor.Decrypt (qrEncryptedString, "password");
}
My Code in Swift:
func Encrypt(msg:String, pwd:String) -> String{
let data = msg.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let chiper = RNCryptor.encryptData(data!, password: pwd)
let base = chiper.base64EncodedDataWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
let stringBase = String(data: base, encoding: NSUTF8StringEncoding)
return stringBase!
}
func Decrypt(msg:String, pwd:String) -> String{
let encodedData:NSData = NSData(base64EncodedString: msg, options: NSDataBase64DecodingOptions(rawValue: 0))!
do{
let decryptedData = try RNCryptor.decryptData(encodedData, password: pwd)
let decrypytedString = String(data: text, encoding: NSUTF8StringEncoding)
return decryptedString!
}
catch let error as NSError{
print(error.localizedDescription)
print(error.localizedDescription)
}
return "AN ERROR OCCURED"
}
For example:
"ABC", with password "behdad" in C#, Encryptor returned:
"AgHfT2VvVOorlux0Ms47K46fG5lQOP2YhYWq2KeIKh+MisCDqZfrLF+KsJyBR2EBNC3wQpaKev0X4+9uuC5vliVoHkLsEi6ZI7ZIZ8qVUEkYGQ=="
When I decrypt it in C#, it returned "ABC".
But when I pass this Base64Encoded string to my Swift Decryptor function,it returned:
RNCryptorError error 2.
For Example:
qrCodeString = "ABC".
public static string EncryptQRCode returns =
"AgF6P5Ya0SifSymd3LqKdH+kGMCFobiziUhwwB6/lfZgAA9N+F5h350MyigoKo9qgUpMXX3x9FxZXwUOJODL4is3R62EGvZWdJBzjSNCef7Ouw=="
The "msg" is returned data from EncryptQRCode(The Base64 Encoded String).
pwd = "password"
encoded data = <02017a3f 961ad128 9f4b299d dcba8a74 7fa418c0 85a1b8b3 894870c0 1ebf95f6 60000f4d f85e61df 9d0cca28 282a8f6a 814a4c5d 7df1f45c 595f050e 24e0cbe2 2b3747ad 841af656 7490738d 234279fe cebb>
decryptedString and decryptedData do not have values due to the error occurred.
RNCryptorError error 2
UnknownHeader = 2
Unrecognized data format. Usually this means the data is corrupt.
This means that the data passed is not in the correct format.
The best programming advice I ever got was one night in the computer room when I asked Rick Cullman for help and he said: "Read the documentation."
That is why I suggested displaying the inputs and outputs, you will catch that.
There are many places where hexadecimal is need to see what is happening and to debug.
Opened an issue in RNCryptor Swift to add the error codes to the documentation.
In C# when you want to encrypt the text,you have to use Schema.V3 for encryption. Decryptor in Swift cannot identify the Schema Version of Base 64 encoded string.
string encrypted = encryptor.Encrypt (YOUR_PLAIN_TEXT, YOUR_PASSWORD,Schema.V3);

AES encrypt in .NET and decrypt with Node.js crypto?

I'm trying to encrypt some data in Mono C#, send it to a NodeJS server and decrypt it there. I'm trying to figure out what algorithms to use to match the two.
I send the encrypted string encoded with base64. So I do something like this in Javascript, where I know the key which was used to encrypt the data in my C# application:
var decipher = crypto.createDecipher('aes192',binkey, biniv);
var dec = decipher.update(crypted,'base64','utf8');
dec += decipher.final('utf8');
console.log("dec", dec);
In Mono I create my Cypher with:
using System.Security.Cryptography;
using (Aes aesAlg = Aes.Create("aes192"))
I need to pass the correct string to Aes.Create() in order to have it use the same algorithm, but I can't find what it should be. "aes192" is not correct it seems.
I don't need aes192 this was just a tryout. Suggest a different encryption flavor if it makes sense. Security is not much of an issue.
Here are links to .NET and Nodejs docs:
http://msdn.microsoft.com/en-us/library/system.security.cryptography.aes.aspx
http://nodejs.org/api/crypto.html
This code works for my Node.js side, but please replace the static iv, otherwhise aes encryption would be useless.
var crypto = require('crypto');
function encrypt(data, key) {
key = key || new Buffer(Core.config.crypto.cryptokey, 'binary'),
cipher = crypto.createCipheriv('aes-256-cbc', key.toString('binary'), str_repeat('\0', 16));
cipher.update(data.toString(), 'utf8', 'base64');
return cipher.final('base64');
}
function decipher(data, key) {
key = key || new Buffer(Core.config.crypto.cryptokey, 'binary'),
decipher = crypto.createDecipheriv('aes-256-cbc', key.toString('binary'), str_repeat('\0', 16));
decipher.update(data, 'base64', 'utf8');
return decipher.final('utf8');
}
function str_repeat(input, multiplier) {
var y = '';
while (true) {
if (multiplier & 1) {
y += input;
}
multiplier >>= 1;
if (multiplier) {
input += input;
} else {
break;
}
}
return y;
}
I hope this helps You.
NOTE: You need to deliver an 265bit aka 32 character key for this algorithm to work.
POSSIBLE .NET SOLUTION: This may help you Example
You should simply write new AesManaged().
You don't need to call Create().
You then need to set Key and IV, then call CreateDecryptor() and put it in a CryptoStream.
It turned out to be a stupid mistake. I thought the create function in Node.js could take a variable argument count. Turns out you need to call the createDecipheriv() instead.
Just for the record, you can easily check the padding and mode by looking at those properties in the Aes object. The defaults are CBC and PKCS7. That padding is also used in nodejs crypto. So a for a 128 key size my code to decrypt a base64 encoded string would be:
var crypto = require('crypto');
var binkey = new Buffer(key, 'base64');
var biniv = new Buffer(iv, 'base64');
var decipher = crypto.createDecipheriv('aes-128-cbc', binkey, biniv);
var decrypted = decipher.update(crypted,'base64','utf8');
decrypted += decipher.final('utf8');
console.log("decrypted", decrypted);

C# HMAC to Java

I'm making the equivalent java code for the code below. But I can make something that returns the same result for encodedString. What Java class can I use for achieve the same result?
//Set the Hash method to SHA1
HMAC hash;
switch (validation)
{
case MachineKeyValidation.MD5:
hash = new HMACMD5();
break;
case MachineKeyValidation.SHA1:
default:
hash = new HMACSHA1();
break;
}
//Get the hash validation key as an array of bytes
hash.Key = HexToByte(validationKey);
//Encode the password based on the hash key and
//converts the encrypted value into a string
encodedString = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
Thanks in advance!
:)
I found a solution for the translation code.
There was two main problem. When a request a HMACSHA1 I'm not talking about a SHA1 algorithm, but a HmacSHA1. And there is a difference between the encoding from Java and C#. I was using the correct key, and the correct algorithm, but the encoding was differente.
SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
// The big problem is difference between C# and Java encoding
byte[] rawHmac = mac.doFinal(data.getBytes("UTF-16LE"));
result = new String(Base64.encode(rawHmac));
See this question about computing hash functions in Java.
And look at the javadoc for java.security.MessageDigest.getInstance(String algorithm).
Edited to add:
Try running the following app to see what providers you have registered.
import java.security.Provider;
import java.security.Security;
public class SecurityTest {
public static void main(String[] args) {
Provider[] providers = Security.getProviders();
for (Provider p : providers) {
System.out.println(p.toString());
}
}
}
You should have at least a few Sun providers listed. If not, you may need to download some security libraries.

Categories

Resources