Implement new SHA256 signature for Redsys/Sermepa in Python - python

I have a django-based application that uses Redsys (earlier Sermepa) as a credit card payment platform. Recently we have received a message that urge us to migrate from old SHA1 signatures to SHA256 in order to keep using the platform securely.
Platform provide several examples of how to make the new requests generating a new signature, but only using PHP and Java. Now I am trying to port the provided solutions to Python without success.
This is a sample working code in PHP (with fake key/data):
<?php
// Init form with default data
$order = "ABCDEFGHI";
$merchant_parameters = array(
"DS_MERCHANT_AMOUNT" => "1000",
"DS_MERCHANT_ORDER" => strval($order) ,
"DS_MERCHANT_MERCHANTCODE" => "012345678",
"DS_MERCHANT_CURRENCY" => "978",
"DS_MERCHANT_TRANSACTIONTYPE" => "0",
"DS_MERCHANT_TERMINAL" => "1",
);
// Key definition, decoding, and encrypt using $order
$key = 'Bg8HYhVT79PDvOxbI/Newcm31QY/9999';
echo 'key --> ' , $key , '<br/><br/>';
$key_decoded = base64_decode($key);
echo 'key-decoded --> ' , $key_decoded , '<br/><br/>';
$ciphertext = mcrypt_encrypt(MCRYPT_3DES, $key_decoded, $order, MCRYPT_MODE_CBC, implode(array_map("chr", array(0,0,0,0,0,0,0,0))) );
echo 'ciphertext --> ' , $ciphertext , '<br/><br/>';
// Transform array in encoded json
$parameters = json_encode($merchant_parameters);
echo 'parameters --> ' , $parameters , '<br/><br/>';
$parameters_encoded = base64_encode($parameters);
echo 'parameters_encoded --> ' , $parameters_encoded , '<br/><br/>';
// Calculate MAC256 of encoded array
$mac_256 = hash_hmac('sha256', $parameters_encoded, $ciphertext, true);
echo 'mac_256 --> ' , $mac_256 , '<br/><br/>';
// Encode MAC256 in base64 to get the signature
$mac_256_encoded = base64_encode($mac_256);
echo 'mac_256_encoded --> ' , $mac_256_encoded , '<br/><br/>';
?>
And this is my equivalent code in Python, with the same fake data:
# -*- encoding: utf-8 -*-
from pyDes import triple_des, CBC, PAD_PKCS5
import hashlib, json, base64, hmac
from json import JSONEncoder
# Init form with default data
order = "ABCDEFGHI"
merchant_parameters = {}
merchant_parameters['DS_MERCHANT_AMOUNT'] = "1000"
merchant_parameters['DS_MERCHANT_ORDER'] = order
merchant_parameters['DS_MERCHANT_MERCHANTCODE'] = "012345678"
merchant_parameters['DS_MERCHANT_CURRENCY'] = "978"
merchant_parameters['DS_MERCHANT_TRANSACTIONTYPE'] = "0"
merchant_parameters['DS_MERCHANT_TERMINAL'] = "1"
# Key definition, decoding, and encrypt using order
key = 'Bg8HYhVT79PDvOxbI/Newcm31QY/9999'
print '\n\nkey --> %s' % key
key_decoded = base64.b64decode(key)
print '\n\nkey_decoded --> %s' % key_decoded
k = triple_des(key_decoded, CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5)
ciphertext = k.encrypt(order)
print '\n\nciphertext --> %s' % ciphertext
# Transform array in encoded json (with no lines
parameters = (json.dumps(merchant_parameters)).encode()
print '\n\nparameters --> %s' % parameters
parameters_encoded = ''.join(unicode(base64.encodestring(parameters), 'utf-8').splitlines())
print '\n\nparameters_encoded --> %s' % parameters_encoded
# Calculate MAC256 of encoded array
mac_256 = hmac.new(ciphertext, parameters_encoded, digestmod=hashlib.sha256).hexdigest()
print '\n\nmac_256 --> %s' % mac_256
# Encode MAC256 in base64 to get the signature
mac_256_encoded = base64.b64encode(mac_256)
print '\n\nmac_256_encoded --> %s' % mac_256_encoded
As can be seen if running the codes, resultant signatures are not equivalent, thus the payment platform rejects my Python request.
Can anybody notice where am I wrong regarding the signature generation?
Thanks in advance,
Álvaro.

Got the same problem.
Actually, after JSON encoding I have had to clean the blank spaces between key/value pairs up.
But I am getting different signatures for the ciphertext. That causes that final signature is different from the examples.
Notice that in example code they do a trailing pad for order number in the 3DES encryption.
iv = b'\0\0\0\0\0\0\0\0'
k = DES3.new(key, DES3.MODE_CBC, iv)
ceros = b'\0'*(len(DS_MERCHANT_ORDER)%8)
clave = k.encrypt(DS_MERCHANT_ORDER+ceros.encode('UTF-8'))
Thanks
EDITED:
Finally I fixed it:
To get the specific per operation key:
iv = b'\0\0\0\0\0\0\0\0'
k = DES3.new(key, DES3.MODE_CBC, iv)
ceros = b'\0'*(len(DS_MERCHANT_ORDER)%8)
claveOp = k.encrypt(DS_MERCHANT_ORDER+ceros.encode('UTF-8'))
To get the hmac.sha256:
from Crypto.Hash import HMAC, SHA256
# Realizo la codificacion SHA256
dig = HMAC.new(claveOp , msg=DS_MERCHANT_PARAMETERS, digestmod=SHA256).digest()
signatureEnc = base64.b64encode(dig)
Hope it helps you

Hie,
finally I publish in pypi new version about redsys client. https://pypi.python.org/pypi/redsys/
Thanks Álvaro about to do sha256 signature.

Related

Exchanging encrypted messages between python and swift

I need to have a python code and a swift code exchange encrypted message.
Here's what I tried:
Fernet
After a review of the options, I thought that a symetric key algorithm could work well.
In python (as usual), it is straightforward to encrypt and decrypt:
Fernet(key).encrypt(b"mdg") # encrypt
Fernet(key).decrypt(encryptedMsg) # decrypt
In swift, it seemed initially straightforward with something along the lines of:
func encrypt(key: String, msg: String) throws -> String {
let data = Data(base64URL: key)!
let symetricKey = try! SymmetricKey(data: d)
let msgUtf8 = msg.data(using: .utf8)!
let sealBox = try! AES.GCM.seal(msgUtf8, using: symetricKey, nonce: nil)
return sealBox.combined.base64EncodedString();
}
However, I have been unable to find the algorithm in swift matching python's Fernet.
ChaCha
While searching for the problem, I landed on this amazing answer from Bram. Very unfortunately it only solves one side of my problem : encrypting messages in python and decoding them in swift. I also need the reverse process.
How to solve this?
To start, we first need a way to create secure random values to generate the IV and keys. You can also generate the keys using CryptoKit's SymmetricKey and extract the data from them, but for now, I'll use this function.
extension Data {
static func secureRandom(ofSize size: Int) -> Data {
var output = [UInt8](repeating: 0, count: size)
_ = SecRandomCopyBytes(kSecRandomDefault, size, &output)
return Data(output)
}
}
We then require the possibility to compute the AES CBC ciphertext, which can be done using CommonCrypto.
func encrypt(plaintext: Data, key: Data, iv: Data) -> Data {
var encryptor: CCCryptorRef?
defer {
CCCryptorRelease(encryptor)
}
var key = Array(key)
var iv = Array(iv)
var plaintext = Array(plaintext)
CCCryptorCreate(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES), CCOperation(kCCOptionPKCS7Padding), &key, key.count, &iv, &encryptor)
var outputBytes = [UInt8](repeating: 0, count: CCCryptorGetOutputLength(encryptor, plaintext.count, false))
CCCryptorUpdate(encryptor, &plaintext, plaintext.count, &outputBytes, outputBytes.count, nil)
var movedBytes = 0
var finalBytes = [UInt8](repeating: 0, count: CCCryptorGetOutputLength(encryptor, 0, true))
CCCryptorFinal(encryptor, &finalBytes, finalBytes.count, &movedBytes)
return Data(outputBytes + finalBytes[0 ..< movedBytes])
}
and the HMAC with the SHA-256 hash function. I recommend using CryptoKit's HMAC implementation here, but to keep things simple, I went with the CommonCrypto implementation.
func computeHMAC(_ data: Data, using key: Data) -> Data {
var data = Array(data)
var key = Array(key)
var macOut = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), &key, key.count, &data, data.count, &macOut)
return Data(macOut)
}
This brings all of this together into the following
let plaintext = Data("Hello world!".utf8)
let signingKey = Data.secureRandom(ofSize: kCCKeySizeAES128)
let cryptoKey = Data.secureRandom(ofSize: kCCKeySizeAES128)
let fernetKey = (signingKey + cryptoKey).base64EncodedString()
let version: [UInt8] = [0x80]
let timestamp: [UInt8] = {
let timestamp = Int(Date().timeIntervalSince1970).bigEndian
return withUnsafeBytes(of: timestamp, Array.init)
}()
let iv = Data.secureRandom(ofSize: kCCBlockSizeAES128)
let ciphertext = encrypt(plaintext: plaintext, key: cryptoKey, iv: iv)
let hmac = computeHMAC(version + timestamp + iv + ciphertext, using: signingKey)
let fernetToken = (version + timestamp + iv + ciphertext + hmac).base64EncodedString()
print("Fernet key: \(fernetKey)")
print("Fernet token: \(fernetToken)")
An example output can be
Fernet key: 7EwFlYNKTGfj+2fSgL3AUqtrRqRs4D1TWNK7t2XbGJQ=
Fernet token: gAAAAABivCLM0y0poDtGOohT1yK4XTDJppYPJdu4fuDTZ5tb9P9KP5ACgX8aJq4imsSdbzOCcvY3Tueo4FYbwyG+ZugozILL+Q==
We can use this in python using cryptography.io's implementation
from cryptography.fernet import Fernet
key = b'7EwFlYNKTGfj+2fSgL3AUqtrRqRs4D1TWNK7t2XbGJQ='
token = b'gAAAAABivCLM0y0poDtGOohT1yK4XTDJppYPJdu4fuDTZ5tb9P9KP5ACgX8aJq4imsSdbzOCcvY3Tueo4FYbwyG+ZugozILL+Q=='
Fernet(key).decrypt(token)
# b'Hello world!'

How to generate Terra's private key like the one Terra Station Wallet produces?

Does anyone know how the Terra Station Wallet generates the 364 characters Private Key? I am looking for a way to generate this 364 characters Private Key using terra-sdk, but the length of the mk = MnemonicKey()'s mk.private_key is not 364 characters.
Appreciate any help
You sparked my curiosity. I went through the Terra Station code (I chose mobile) to see how they do it. I first searched for where in the UI was the “Export Private Key”; it looks to be the encrypted Key string in AuthDataValueType.
Here’s where they read it out of keystore. https://github.com/terra-money/station-mobile/blob/f74c4224986fd9ed32b4380b537e9ae13ca05c3e/src/utils/authData.ts#L15
Here’s where they create it for a newly recovered wallet. https://github.com/terra-money/station-mobile/blob/3ec15b9a620432dee47378f5b6e621d93780748a/src/utils/wallet.ts#L66
And, lastly here are the encrypt util functions. https://github.com/terra-money/station-mobile/blob/3ec15b9a620432dee47378f5b6e621d93780748a/src/utils/crypto.ts
This is all NodeJS/ReactNative code so you would need to create the same encrypt/decrypt, password and storage flow in Python, if necessary.
The import 'key' for Terra Station is actually a base64 encoded JSON object containing the wallet name, Terra address and the private key (which is further AES encrypted and base64 encoded). This is some C# to create it (you will need to get the private key using something like the Mnemonic Code Converter webpage) - fill in the string variables at the top:
string privatekey = #"";
string walletName = #"";
string address = #"";
string password = #"changeme";
byte[] salt = Encoding.UTF8.GetBytes("kopwemdmondawfwa");
byte[] iv = Encoding.UTF8.GetBytes("dgfdkfsokwedopmf");
int iterations = 100;
int keySize = 256;
var myRijndael = new RijndaelManaged();
myRijndael.KeySize = keySize;
myRijndael.IV = iv;
var rfc2898 = new Rfc2898DeriveBytes(System.Text.Encoding.UTF8.GetBytes(password), salt, iterations);
byte[] key = rfc2898.GetBytes(keySize / 8);
myRijndael.Key = key;
myRijndael.Padding = PaddingMode.PKCS7;
myRijndael.Mode = CipherMode.CBC;
ICryptoTransform transform = myRijndael.CreateEncryptor();
byte[] bak = new System.Text.UTF8Encoding().GetBytes(privatekey);
byte[] encrypted = transform.TransformFinalBlock(bak, 0, bak.Length);
string saltStr = BitConverter.ToString(salt).Replace("-", "");
string ivStr = BitConverter.ToString(iv).Replace("-", "");
string cipherStr = System.Convert.ToBase64String(encrypted);
string keyString = saltStr + ivStr + cipherStr;
string res = "{ \"name\":\"" + walletName + "\",\"address\":\"" + address + "\",\"encrypted_key\":\"" + keyString + "\"}";
byte[] resBytes = new System.Text.UTF8Encoding().GetBytes(res);
string base64 = System.Convert.ToBase64String(resBytes);
File.WriteAllText(#"c:\temp\ts_exported_key.txt", base64);

RSA signing with Python verifying with C#

I'm trying to sign message with RSA in Python and then verifying data in C#, but getting fail still after hours of testing/fighting.
Python code for signing:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
message = b"Test string"
with open("id_rsa", "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None,
)
signature = private_key.sign(
message,
padding.PKCS1v15(),
hashes.SHA256()
)
signaturefile = open('signed.dat', 'wb')
signaturefile.write(signature)
signaturefile.close()
datafile = open('message.dat', 'wb')
datafile.write(message)
datafile.close()
And C# code for verifying:
private void button_Click(object sender, EventArgs e)
{
byte[] data = File.ReadAllBytes("message.dat");
byte[] signature = File.ReadAllBytes("signed.dat");
try
{
using (var reader = File.OpenText("id_rsa.pub"))
{
var pem = new PemReader(reader);
var o = (RsaKeyParameters)pem.ReadObject();
using (var rsa = new RSACryptoServiceProvider())
{
var parameters = new RSAParameters();
parameters.Modulus = o.Modulus.ToByteArray();
parameters.Exponent = o.Exponent.ToByteArray();
rsa.ImportParameters(parameters);
bool ok = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
if (ok) Console.WriteLine("Verified");
}
}
}
catch (CryptographicException exc)
{
Console.WriteLine(exc.Message);
}
}
I'm getting always fail from rsa.VerifyData. Could someone point me what is the problem?
Same files can be verified well in Python.
RSAParameters#Modulus and RSAParameters#Exponent expect modulus and exponent unsigned, so Org.BouncyCastle.Math.BigInteger#ToByteArrayUnsigned() must be used instead of Org.BouncyCastle.Math.BigInteger#ToByteArray(). With this change, verification is successful.
Also, the big endian byte order is required, but this applies to both methods.
Note that System.Numerics.BigInteger#ToByteArray() returns the data signed with little endian order.
Your current approach uses BC for key import and built-in .NET methods for verification. Alternatively, BC classes can also be used for verification, making for a slightly more efficient implementation overall.
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
...
ISigner signer = SignerUtilities.GetSigner("SHA256withRSA");
signer.Init(false, o);
signer.BlockUpdate(data, 0, data.Length);
bool verified = signer.VerifySignature(signature);

AES encrypt in python decrypt in php

I am asking this because I don't really know php but must to somehow manage with.
I have encrypted data in python and need to decrypt in php (serversite).
python encryption:
import hashlib
from base64 import b64encode, b64decode, urlsafe_b64encode, urlsafe_b64decode
from Crypto.Cipher import AES
text = "secret"
secret_key = 'This is my secret key'
secret_iv = 'This is my secret iv'
key = hashlib.sha256(secret_key.encode('utf-8')).hexdigest()[:32].encode("utf-8")
iv = hashlib.sha256(secret_iv.encode('utf-8')).hexdigest()[:16].encode("utf-8")
_pad = lambda s: s + (AES.block_size - len(s) % AES.block_size) * chr(AES.block_size - len(s) % AES.block_size)
txt = _pad(text)
cipher = AES.new(key, AES.MODE_CBC, iv)
output = urlsafe_b64encode(cipher.encrypt(str.encode(txt))).rstrip(b'=')
this gives 'rtVabOaDdf528T63xOhhww' output, which is correctly AES encrypted.
and php which encrypts and decrypts in other way:
<?php
$string="secret";
class CryptService{
private static $encryptMethod = 'AES-256-CBC';
private $key;
private $iv;
public function __construct(){
echo '<br>: '.$this->key = substr(hash('sha256', 'This is my secret key'), 0, 32);
echo '<br>: '.$this->iv = substr(hash('sha256', 'This is my secret iv'), 0, 16).'<br>';
}
public function decrypt($string){
// $string = strtr($data, '-_', '+/');
$string = base64_decode($string);
return openssl_decrypt($string, self::$encryptMethod, $this->key, 0, $this->iv);
}
public function encrypt($string){
$output = openssl_encrypt($string, self::$encryptMethod, $this->key, 0, $this->iv);
$output = base64_encode($output);
return $output;
}
}
$a = new CryptService;
echo $ok=$a->encrypt('secret');
echo "\n";
echo 'TEST: '.$a->decrypt($string);
echo 'BACK ok: '.$a->decrypt($ok);
echo "\n\n";
There is some issue with openssl_decrypt() function because of "iv". Can someone help me figure this out...
You're assigning additional 4 characters <br> to your $this->iv. This will fix it:
echo '<br>: ' . ($this->iv = substr(hash('sha256', 'This is my secret iv'), 0, 16)) . '<br>';
Basically, your . '<br>' is concatenating the <br> to your substr(). I added () around the variable value assignment. Now it works
cnRWYWJPYURkZjUyOFQ2M3hPaGh3dz09 TEST: BACK ok: secret
I am not an expert on encryption, but... I think there's something in your code that doesn't quite belong in there. When I remove these two lines:
$string = base64_decode($string);
$output = base64_encode($output);
I get this output:
rtVabOaDdf528T63xOhhww==
Which, after a rtrim($ok, '=');, would give you
rtVabOaDdf528T63xOhhww

Node Crypto aes-128-ecb encrypt doesnt get me right results

Following program in python:
from Crypto.Cipher import AES
key = '11223344556677889900aabbccddeeff'.decode("hex")
aesECB = AES.new(key, AES.MODE_ECB)
ciphertext = aesECB.encrypt('1234567890abcdef')
print ciphertext.encode('base64')
Gets me this result:
$ python example_cipher.py
r9yD3EmmAIpxncxZSldsKg==
Following command line from openssl, gets me the same result:
$ echo -n "1234567890abcdef" | openssl aes-128-ecb -K 11223344556677889900aabbccddeeff -nopad | openssl base64
r9yD3EmmAIpxncxZSldsKg==
But this code in Node:
var crypto = require('crypto');
var key = new Buffer('11223344556677889900aabbccddeeff', 'hex');
var plaintext = new Buffer('1234567890abcdef', 'utf8');
var cipher = crypto.createCipher("aes-128-ecb", key);
cipher.setAutoPadding(false);
var ciphertext = cipher.update(plaintext, 'utf8');
console.log(ciphertext.toString('base64'));
Doesn'g gets me the same result:
$ node cipher
tOunZRvle8B6HYuBSzblqw==
Where is the mistake?
First off, DO NOT use ECB if you are in control of the crypto parameters. It is incredibly insecure and should not be used to encrypt data.
Now for ECB, which technically does not use an IV, you still need to use crypto.createCipheriv() with a zero-length IV Buffer to match the output of Python:
var cipher = crypto.createCipheriv("aes-128-ecb", key, Buffer.alloc(0));
Also (in general), you are missing cipher.final() to include any leftover data, so use this instead:
var ciphertext = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final()
]);

Categories