I am attempting to encrypt a string for passing to SagePay, but so far my attempts are rejected by SagePay saying that the encryption method I have used is not supported.
Their doc says: This string should be encrypted using the AES/CBC/PCKS#5 algorithm and the pre-registered Encryption password, then subsequently Base64 encoded to allow safe transport in an HTML form.
The password they provide looks a bit like this: 8JQc4w5MUsZ47Z8z
Here is some code I cobbled together to encrypt my plainTextString using my passwordString
I didn't know how to convert the password they provide to a SecretKey used by Cipher, so there are guesses in there.
SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
byte salt = new byte;
PBEKeySpec keySpec = new PBEKeySpec(passwordString.toCharArray(), salt, 65536, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
PBEKey key = (PBEKey) factory.generateSecret(keySpec);
SecretKey secretKey = new SecretKeySpec(key.getEncoded(), "AES");
byte byteCipherText = aesCipher.doFinal(byteDataToEncrypt);
crypt = new BASE64Encoder().encode(byteCipherText);
System.out.println("Cipher Text generated using AES is " + crypt);
Have SagePay provided enough info for me to achieve the encryption; they have basically just told me that I must be doing it wrong and they can't help and certainly wouldn't comment on any code! They do provide a Java api, but then the bit that does the encryption is as they put it "locked down" so they refuse to provide a class/method in the API to do the encryption/decryption for me!
As you seem to appreciate, just saying that one should use AES/CBC/PCKS#5 is not enough. You will also need from them
a) the algorithm used to convert the Encryption password to a 16 byte AES key,
b) the IV used to initialise the CBC.
You have assumed that the key is generated using PBE based on PBKDF2WithHmacSHA1. This may or may not be correct but even if it is you need to know the salt and the iteration count. The salt can't be a random value as you have assumed!
If the SagePay key looks like 8JQc4w5MUsZ47Z8z (I assume that is not the real key) and if it is exactly 16 characters long then there is a chance that the key bytes are actually just the ASCII bytes of 8JQc4w5MUsZ47Z8z and can be obtained using "8JQc4w5MUsZ47Z8z".getBytes("ASCII") .
I'm betting that SagePay assume an IV of 16 bytes of zero (this is a very common failing) which introduces a small security weakness. It means that one will be able to tell if two encrypted messages longer than 16 byte start with the same content! This weakness would be removed if the IV is a random value and shipped with the ciphertext but I bet that SagePay don't use this approach.
In your position I would first try using "8JQc4w5MUsZ47Z8z".getBytes("ASCII") as the key with an IV of 16 bytes of zero. If this fails I would put in a call to SagePay and ask them to come clean.
Note that if SagePay do use "8JQc4w5MUsZ47Z8z".getBytes("ASCII") as the key then there effective key length is significantly shorter than 128 bits; it will be more like 95 bits (approx 16*log2(62)) .
Richard, I really appreciate your detailed reply. I have passed the questions to SagePay, though from past experience I don't hold out hopes of a quick / detailed reply. You mentioned something to try below, but without the salt it sounds like trying anything would be futile? This suggests to me that they want clients to use their own code to do the encryption, but they have hidden the encryption bit of their own API / not documented it / will not answer questions about it, perhaps in their eyes for security reasons. They instead provided a J2EE sample application, but I don't use a J2EE app framework.
You don't need a salt or iteration count if the key is just the ASCII bytes of the password and the IV is 16 bytes of zero! You can get rid of all your current PBE key generation and just use the bytes of the passphrase. The change is very very simple -
The response from SagePay is once again predictable; they say the only information they provide is the password! They seem to view their system API and documentation as not really theirs, or at least not something their customer service team can touch with a barge pole. Hopeless!
The do provide code here including Java http://www.sagepay.co.uk/support/find-an-integration-document/form-integration The Java integration kit is however actually a sample J2EE application which I am not familiar with. I was unable to isolate the bit where the encryption/decryption is actually done as much of the download does not provide source, the source code is just the J2EE sample application bits from what I can see. There is probably a simple answer lurking in there somewhere. I thought I had found it when I located a byte aesEncrypted = CryptographyHelper.AESEncrypt(detail, "ISO-8859-1", webSite.encryptionPassword()); method, but the end result was the same error once they receive the encrypted data. SagePay are unable to say whether I located the correct class / method or not or exactly how to use it.
There is some Java source. Maybe not the whole Java API source but certainly not just example source. A quick looks at the source and documentation make me think that the encryption is the least of your worries! If you are not going to learn to use SagePay API then there is a whole protocol to implement. No small task. I think you are going to have to bite the bullet and spend significant time learning to use the SagePay API. J2EE ain't that hard and from a quick look at the examples the API seem easy enough to understand !
Richard, our software actually integrates with SagePay already using their previously supported encryption method of Xor, which is no longer supported. The API is simple enough, the data is all there, the only bit that needs upgrading is the encryption/decryption method. Unfortunately they hide that bit away as far as I can make out. From the link I posted above, someone knows a bit more than SagePay are telling me about their encryption process! Thanks, John
I was quite hopeful of the keyBytes and ivBytes being the same, but still not going through. I notice the code also mentions Rijndael which I tried in place of AES for both the SecretKeySpec construction and the Cipher.getInstance() but nether worked. I can't help thinking I am getting close. I'm suspicious about some "@" at the beginning which I saw in some other code somewhere else.
This also shows similar: http://stackoverflow.com/questions/13360079/php-mcrypt-equivalent-for-sagepay-on-a-windows-server Again indicating that sagepay uses the same string for key and iv
Also there is that "@" being added to the front of the result.
I tried adding the "@" and the error changed…no longer saying the encryption was wrong, but complaining about a missing currency field in the unencrypted data, which I know is present.
What about the Rijndael and 128 translated to Java?
//** AES encryption, CBC blocking with PKCS5 padding then HEX encoding - DEFAULT **
//** use initialization vector (IV) set from $strEncryptionPassword
$strIV = $strEncryptionPassword;
//** add PKCS5 padding to the text to be encypted
$strIn = addPKCS5Padding($strIn);
What about the Rijndael and 128 translated to Java?;
When the tender went out for AES there were several proposed encryption algorithms and the winner was Rijndael limited to 128 bit block size. So AES is Rijndael with block size of 128 bits.
The chances of the decryption working using the wrong key and IV and then providing a meaningful decrypted output are are effectively zero so the fact that you are getting an error message saying that a currently is missing is encouraging. It means you have probably decrypted correctly. You say the currency field is not missing but unless the decryption software is rubbish I would doubt this; you have only a 1 in 255 chance of the decrypted PKCS5 padding being valid for an arbitrary key and/or IV.
SagePay have confirmed that they can't decrypt at their end still, so the change in error message is just a bit confusing; they are looking for "Currency" in a garbage string and not finding it. I will post back if/when I can resolve this.
The change that appeared to get me over he line was to use Hex.encodeHexString() instead of Base64.encodeBase64(), which I spotted in another forum posting.
Even easier, I was then able to go back to using the SagePay API with that step added:
Richard, you have been a huge help to me. If you have anything for me to pass on to SagePay, please do let me know! I don't understand security and encryption, but I don't see that I should need to; I just need a working and documented API provided by SagePay.