I'm developing an iOS app that makes payments through a TPV Redsys API. I'm following the documents but it's not working (server returns error due to an incorrect signature) and I guess it's because of the 3DES encryption.
I'm using the test data from the documentation so the output should be the same as in the documentation. This is my code:
- (void) payViaTPVWithAmount:(NSString *)amount andOrderId:(NSString *)orderId
{
// I don't use my amount and orderId but those provided by the documentation to test
// We need to obtain 3 fields: DS_SIGNATURE_VERSION, DS_MERCHANTPARAMETERS and DS_SIGNATURE.
// 1) Obtaining DS_SIGNATURE_VERSION
NSString *dsSignatureVersion = @"HMAC_SHA256_V1";
// >>>> This step is easy and of course is correct <<<<
// 2) Obtaining DS_MERCHANTPARAMETERS
// 2.1) Build params JSON -using test data-
NSString *params = [NSString stringWithFormat: @"{\"DS_MERCHANT_AMOUNT\":\"%@\",\"DS_MERCHANT_ORDER\":\"%@\",\"DS_MERCHANT_MERCHANTCODE\":\"%@\",\"DS_MERCHANT_CURRENCY\":\"978\",\"DS_MERCHANT_TRANSACTIONTYPE\":\"0\",\"DS_MERCHANT_TERMINAL\":\"%@\",\"DS_MERCHANT_MERCHANTURL\":\"%@\",\"DS_MERCHANT_URLOK\":\"%@\",\"DS_MERCHANT_URLKO\":\"%@\"}",
@"145",
@"1446117555",
@"327234688",
@"1",
@"http:\\/\\/www.bancsabadell.com\\/urlNotificacion.php",
@"http:\\/\\/www.bancsabadell.com\\/urlOK.php",
@"http:\\/\\/www.bancsabadell.com\\/urlKO.php"];
//2.2) convert JSON params to base64
NSData *paramsData = [params dataUsingEncoding:NSUTF8StringEncoding];
NSString *paramsBase64 = [paramsData base64EncodedStringWithOptions:0];
NSLog(@"apiMacSHA256\n%@", params);
NSLog(@"apiMacSHA256Base64\n%@", paramsBase64);
// >>>>> This output is identical to that included in the documents so this step 2 is ok. <<<<<
// 3) Obtaining DS_SIGNATURE
// 3.1) start with secret key -the document gives this for test purposes-
NSString *merchantKey = @"sq7HjrUOBfKmC576ILgskD5srU870gJ7"; //32 bytes
// 3.2) convert it to base64
NSData *merchantKeyData = [merchantKey dataUsingEncoding:NSUTF8StringEncoding];
NSString *merchantKeyBase64 = [merchantKeyData base64EncodedStringWithOptions:0];
// 3.3) the documentation doesn't say to convert this key to Hex but I have the Android code (which is working) and it has this step.
// Anyway I've tested with both options, with this step and without this step.
NSData *merchantKeyBase64Data = [merchantKeyBase64 dataUsingEncoding:NSUTF8StringEncoding];
NSString *merchantHex = [merchantKeyBase64Data hexadecimalString];
// 3.4) get 3DES from orderId and base 64 (or Hex depending on if step 3.3 is done) key just obtained
NSData *operationKey = [self encrypt_3DSWithKey:merchantHex andOrderId:@"1446117555"];
// 3.5) get HMAC SHA256 from operationkey and ds_merchantparameters
NSData *signatureHmac256 = [self mac256WithKey:operationKey andString:paramsBase64];
// 3.6) convert HMAC SHA256 to base64
NSString *signatureHmac256Base64 = [signatureHmac256 base64EncodedStringWithOptions:0];
NSLog(@"signatureHmac256Base64\n%@", signatureHmac256Base64);
// This step is not working, the signature is not the correct one and the connection to the TPV Redsys system fails.
// 4) Build the request
// Set URL - using URL for development purposes-
NSURL *url = [NSURL URLWithString:@"https://sis-t.redsys.es:25443/sis/realizarPago"];
NSString *dsSignatureVersionEncoded = [dsSignatureVersion stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLUserAllowedCharacterSet]];
NSString *paramsHTTPEncoded = [paramsBase64 stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLUserAllowedCharacterSet]];
NSString *signatureHmac256HTTPEncoded = [signatureHmac256Base64 stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLUserAllowedCharacterSet]];
NSString *body = [NSString stringWithFormat:@"DS_SIGNATURE=%@&DS_MERCHANTPARAMETERS=%@&DS_SIGNATUREVERSION=%@", signatureHmac256HTTPEncoded, paramsHTTPEncoded, dsSignatureVersionEncoded];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL: url];
[request setHTTPMethod: @"POST"];
[request setHTTPBody: [body dataUsingEncoding: NSUTF8StringEncoding allowLossyConversion:false]];
[_webViewer loadRequest:request];
}
- (NSData *)encrypt_3DSWithKey:(NSString *)key andOrderId:(NSString *)str
{
NSData *plainData = [str dataUsingEncoding:NSUTF8StringEncoding];
NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
size_t bufferSize = plainData.length + kCCKeySize3DES;
NSMutableData *cypherData = [NSMutableData dataWithLength:bufferSize];
size_t movedBytes = 0;
CCCryptorStatus ccStatus;
ccStatus = CCCrypt(kCCDecrypt,
kCCAlgorithm3DES,
kCCOptionECBMode,
keyData.bytes,
kCCKeySize3DES,
NULL,
plainData.bytes,
plainData.length,
cypherData.mutableBytes,
cypherData.length,
&movedBytes);
cypherData.length = movedBytes;
if(ccStatus == kCCSuccess)
{
NSLog(@"Data: %@",cypherData);
}
else
{
NSLog(@"Failed DES decrypt, status: %d", ccStatus);
}
return cypherData;
}
- (NSData *)mac256WithKey:(NSData *)key andString:(NSString *)str
{
NSData *strData = [str dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData* hash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH ];
CCHmac(kCCHmacAlgSHA256, key.bytes, key.length, strData.bytes, strData.length, hash.mutableBytes);
return hash;
}
This version fails because I get an encryption error due to alignment, so I modified the encryption to this:
NSString *initVec = @"\0\0\0\0\0\0\0\0";
const void *vinitVec = (const void *) [initVec UTF8String];
CCCryptorStatus ccStatus;
ccStatus = CCCrypt(kCCDecrypt,
kCCAlgorithm3DES,
kCCOptionPKCS7Padding | kCCOptionECBMode,
keyData.bytes,
kCCKeySize3DES,
vinitVec,
plainData.bytes,
plainData.length,
cypherData.mutableBytes,
cypherData.length,
&movedBytes);
This time the encryption seems to be success but the output is not the expected one.
I've tried with the code from this SO Question but it fails too (incorrect signature and the connection fails).
Is there any error with this code?
I know that is difficult but someone that had used TPV Redsys API to make payments successfully could see what's the problem?
EDIT 1:
This is what the documentation says about how to get DS_SIGNATURE:
Sign the request data
Once you have the string of data to be signed and the terminal specific key, the signature has to be calculated following these steps:
A specific key is generated per operation. To obtain the specific key to be used in an operation, it must be performed a 3DES encryption between the merchant key, which must be previously decoded in BASE 64, and the value of the order Id (Ds_Merchant_Order).
The HMAC SHA256 has to be calculated from the parameter value Ds_MerchantParameters and the key obtained in the previous step.
The result obtained has to be coded in BASE 64, and the result of this coding will be the value of the Ds_Signature parameter.
And since I've got the Redsys API SDK for Java (there is no SKD for iOS) this is what it does to generate Ds_Signature parameter:
public String createMerchantSignature(final String merchantKey) throws Exception {
String merchantParams = createMerchantParameters();
byte [] key = decodeB64(merchantKey.getBytes("UTF-8"));
String secretKc = toHexadecimal(key, key.length);
byte [] secretKo = encrypt_3DES(secretKc, getOrder());
// MAC with the operation key "Ko" and then coded in BASE64
byte [] hash = mac256(merchantParams, secretKo);
String res = encodeB64String(hash);
return res;
}
And what the server expects is something like that:
Expected Ds_SignatureVersion
HMAC_SHA256_V1
Expected Ds_MerchantParameters
eyJEU19NRVJDSEFOVF9BTU9VTlQiOiIxNDUiLCJEU19NRVJDSEFOVF9PUkRFUiI6IjE0NDYwNjg1ODEiLCJEU19NRVJDSEFOVF9NRVJDSEFOVENPREUiOiIzMjcyMzQ2ODgiLCJEU19NRVJDSEFOVF9DVVJSRU5DWSI6Ijk3OCIsIkRTX01FUkNIQU5UX1RSQU5TQUNUSU9OVFlQRSI6IjAiLCJEU19NRVJDSEFOVF9URVJNSU5BTCI6IjEiLCJEU19NRVJDSEFOVF9NRVJDSEFOVFVSTCI6Imh0dHA6XC9cL3d3dy5iYW5jc2FiYWRlbGwuY29tXC91cmxOb3RpZmljYWNpb24ucGhwIiwiRFNfTUVSQ0hBTlRfVVJMT0siOiJodHRwOlwvXC93d3cuYmFuY3NhYmFkZWxsLmNvbVwvdXJsT0sucGhwIiwiRFNfTUVSQ0hBTlRfVVJMS08iOiJodHRwOlwvXC93d3cuYmFuY3NhYmFkZWxsLmNvbVwvdXJsS08ucGhwIiwiRFNfTUVSQ0hBTlRfUEFOIjoiNDU0ODgxMjA0OTQwMDAwNCIsIkRTX01FUkNIQU5UX0VYUElSWURBVEUiOiIxNTEyIiwiRFNfTUVSQ0hBTlRfQ1ZWMiI6IjEyMyJ9
With my code I get this exact base64 string, so the code is ok.
Expected Ds_Signature
QfLVUv4nF2Nw7jBAkw0w8H0eRlwh2E1w/ZlKHdA2Sq0=
With my code my Ds_Signature is longer than the expected one: ly2hYyjVlXQF%2FvgdEXBOj0obUdC7r5IERdEpLPSPksA=
EDIT 2:
I've made some changes based on zaph comments. I've removed the encoding for URLs and I've recalculated steps 3.2 and 3.3. This is my code now:
- (void) payViaTPVWithAmount:(NSString *)amount andOrderId:(NSString *)orderId
{
// I don't use my amount and orderId but those provided by the documentation to test
// We need to obtain 3 fields: DS_SIGNATURE_VERSION, DS_MERCHANTPARAMETERS and DS_SIGNATURE.
// 1) Obtaining DS_SIGNATURE_VERSION
NSString *dsSignatureVersion = @"HMAC_SHA256_V1";
// >>>> This step is easy and of course is correct <<<<
// 2) Obtaining DS_MERCHANTPARAMETERS
// 2.1) Build params JSON -using test data-
NSString *params = [NSString stringWithFormat: @"{\"DS_MERCHANT_AMOUNT\":\"%@\",\"DS_MERCHANT_ORDER\":\"%@\",\"DS_MERCHANT_MERCHANTCODE\":\"%@\",\"DS_MERCHANT_CURRENCY\":\"978\",\"DS_MERCHANT_TRANSACTIONTYPE\":\"0\",\"DS_MERCHANT_TERMINAL\":\"%@\",\"DS_MERCHANT_MERCHANTURL\":\"%@\",\"DS_MERCHANT_URLOK\":\"%@\",\"DS_MERCHANT_URLKO\":\"%@\"}",
@"145",
@"1446117555",
@"327234688",
@"1",
@"http:\\/\\/www.bancsabadell.com\\/urlNotificacion.php",
@"http:\\/\\/www.bancsabadell.com\\/urlOK.php",
@"http:\\/\\/www.bancsabadell.com\\/urlKO.php"];
//2.2) convert JSON params to base64
NSData *paramsData = [params dataUsingEncoding:NSUTF8StringEncoding];
NSString *paramsBase64 = [paramsData base64EncodedStringWithOptions:0];
NSLog(@"apiMacSHA256\n%@", params);
NSLog(@"apiMacSHA256Base64\n%@", paramsBase64);
// >>>>> This output is identical to that included in the documents so this step 2 is ok. <<<<<
// 3) Obtaining DS_SIGNATURE
// 3.1) start with secret key -the document gives this for test purposes-
NSString *merchantKey = @"sq7HjrUOBfKmC576ILgskD5srU870gJ7"; //32 bytes
// 3.2) convert it to base64
NSData *merchantKeyData = [merchantKey dataUsingEncoding:NSUTF8StringEncoding];
NSData *merchantKeyBase64Data = [merchantKeyData base64EncodedDataWithOptions:0];
// 3.3) the documentation doesn't say to convert this key to Hex but I have the Android code (which is working) and it has this step.
// Anyway I've tested with both options, with this step and without this step.
NSString *merchantHex = [merchantKeyBase64Data hexadecimalString];
// 3.4) get 3DES from orderId and base 64 (or Hex depending on if step 3.3 is done) key just obtained
NSData *operationKey = [self encrypt_3DSWithKey:merchantHex andOrderId:@"1446117555"];
// 3.5) get HMAC SHA256 from operationkey and ds_merchantparameters
NSData *signatureHmac256 = [self mac256WithKey:operationKey andString:paramsBase64];
// 3.6) convert HMAC SHA256 to base64
NSString *signatureHmac256Base64 = [signatureHmac256 base64EncodedStringWithOptions:0];
NSLog(@"signatureHmac256Base64\n%@", signatureHmac256Base64);
// This step is not working, the signature is not the correct one and the connection to the TPV Redsys system fails.
// 4) Build the request
// Set URL - using URL for development purposes-
NSURL *url = [NSURL URLWithString:@"https://sis-t.redsys.es:25443/sis/realizarPago"];
NSString *body = [NSString stringWithFormat:@"DS_SIGNATURE=%@&DS_MERCHANTPARAMETERS=%@&DS_SIGNATUREVERSION=%@", signatureHmac256HTTPEncoded, paramsHTTPEncoded, dsSignatureVersionEncoded];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL: url];
[request setHTTPMethod: @"POST"];
[request setHTTPBody: [body dataUsingEncoding: NSUTF8StringEncoding allowLossyConversion:false]];
[_webViewer loadRequest:request];
}
- (NSData *)encrypt_3DSWithKey:(NSString *)key andOrderId:(NSString *)str
{
NSData *plainData = [str dataUsingEncoding:NSUTF8StringEncoding];
NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
size_t bufferSize = plainData.length + kCCKeySize3DES;
NSMutableData *cypherData = [NSMutableData dataWithLength:bufferSize];
size_t movedBytes = 0;
NSString *initVec = @"\0\0\0\0\0\0\0\0";
const void *vinitVec = (const void *) [initVec UTF8String];
CCCryptorStatus ccStatus;
ccStatus = CCCrypt(kCCDecrypt,
kCCAlgorithm3DES,
kCCOptionPKCS7Padding | kCCOptionECBMode,
keyData.bytes,
kCCKeySize3DES,
vinitVec,
plainData.bytes,
plainData.length,
cypherData.mutableBytes,
cypherData.length,
&movedBytes);
cypherData.length = movedBytes;
if(ccStatus == kCCSuccess)
{
NSLog(@"Data: %@",cypherData);
}
else
{
NSLog(@"Failed DES decrypt, status: %d", ccStatus);
}
return cypherData;
}
- (NSData *)mac256WithKey:(NSData *)key andString:(NSString *)str
{
NSData *strData = [str dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData* hash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH ];
CCHmac(kCCHmacAlgSHA256, key.bytes, key.length, strData.bytes, strData.length, hash.mutableBytes);
return hash;
}
My Ds_Signature is now R8+4R6diEbm3nJR6KmonYDy53Zi4CZpuxdoMZtucGX4= (different than the expected one) and the connection still fails.