Using ColdFusion to sign data for single sign-on - encryption

I apologize in advance for the length of this post. I don't really know enough about this issue to properly identify what the specific problem may actually be! But at any rate, we've been making calls against our membership API to query info about our members (join dates, membership types, etc.) using steps and advice provided by #Leigh here and they have been working great! Thanks again, Leigh, our members are very happy to be able to do this!
Now I want to set up single sign-on for our members, allowing them to log in at our page and then be whisked over to their member profile, already logged in on that site. According to the API documentation, one thing I need to do is:
"Use your Signing Certificate to sign the Portal username of the person to log in."
I am totally stuck on this. I've been provided with an XML private key (generated by their .NET application) in the form
<RSAKeyValue><Modulus>{stuff}</Modulus><Exponent>{stuff}</Exponent><P>... etc etc
I gather that I am unable to work with this format directly and must convert it to PEM format or similar. Using OpenSSL, I think I have done this and now have a file in the format of "-----BEGIN PRIVATE KEY-----{stuff}-----END PRIVATE KEY-----."
Using Leigh's solution does give me a signature, but it does not match the example provided in the API docs. I think this is because it uses HmacSHA1, whereas they note that "the signature in the header uses HMAC SHA1 whereas the signature for creating security tokens uses a public/private keypair and RSA-SHA1. The same method cannot be used to generate both." I tried changing
<cfset key = key.init(jKey,"HmacSHA1") />
to
<cfset key = key.init(jKey,"RSA-SHA1") />
and got "Algorithm RSA-SHA1 not available."
I have tried copying and pasting some other suggested solutions, but none of them work. One example (gotten from 12Robots.com):
<!--- Create a Java Cipher object and get a mode --->
<cfset cipher = createObject('java', 'javax.crypto.Cipher').getInstance("RSA") />
<!--- The mode tells the Cipher whether is will be encrypting or decrypting --->
<cfset encMode = cipher.ENCRYPT_MODE />
<cfset encryptedValue = "" /> <!--- Return variable --->
<!--- Initialize the Cipher with the mode and the key --->
<cfset cipher.init(encMode, key) />
<!--- Convert the string to bytes --->
<cfset stringBytes = stringToSign.getBytes("UTF8") />
<!--- Perform encryption --->
<cfset encryptedValue = cipher.doFinal(stringBytes, 0, len(inputString)) />
<cfdump var="#encryptedValue#">
"Key" in this instance is the PEM text I mentioned earlier and "stringToSign" is the username. The error I get is "Either there are no methods with the specified method name and argument types or the init method is overloaded with argument types that ColdFusion cannot decipher reliably. ColdFusion found 0 methods that match the provided arguments. If this is a Java object and you verified that the method exists, use the javacast function to reduce ambiguity."
Another thing I have tried is:
<cfset rsaPrivateKey = toBase64(key, "utf-8")>
<cfset jKey = JavaCast("string", rsaPrivateKey)>
<cfset jMsg = JavaCast("string", stringToSign).getBytes("ASCII")>
<cfset key = createObject("java", "java.security.PrivateKey")>
<cfset keySpec = createObject("java", "java.security.spec.PKCS8EncodedKeySpec")>
<cfset keyFactory = createObject("java", "java.security.KeyFactory")>
<cfset b64dec = createObject("java", "sun.misc.BASE64Decoder")>
<cfset sig = createObject("java", "java.security.Signature")>
<cfset byteClass = createObject("java", "java.lang.Class")>
<cfset byteArray = createObject("java", "java.lang.reflect.Array")>
<cfset byteClass = byteClass.forName(JavaCast("string", "java.lang.Byte"))>
<cfset keyBytes = byteArray.newInstance(byteClass, JavaCast("int", "1024"))>
<cfset keyBytes = b64dec.decodeBuffer(jKey)>
<cfset sig = sig.getInstance("SHA1withRSA", "SunJSSE")>
<cfset sig.initSign(keyFactory.getInstance("RSA").generatePrivate(keySpec.init(keyBytes)))>
<cfset sig.update(jMsg)>
<cfset signBytes = sig.sign()>
<cfset finalSig = ToBase64(signBytes)>
<cfdump var="#finalSig#">
Which gives me "java.security.InvalidKeyException: invalid key format." BTW if I set rsaPrivateKey to just "key" I get a different error, "java.security.InvalidKeyException: IOException : DerInputStream.getLength(): lengthTag=127, too big." I am pleased to be getting different error messages; at least something is happening! :-)
Again, I do not know what these Java functions are doing. And I surely am not getting why something seemingly straightforward has ended up being so complicated! But my suspicion is, I have either stored the private key PEM incorrectly, or am reading out of the database incorrectly (or both), and that is what is contributing to causing these various solutions to fail. But I don't know enough to say for sure if that is the case.
I would welcome any insight or suggestions that might help me out! If anybody needs more info, I am happy to provide that. Thank you all very much in advance!

I gather that I am unable to work with this format directly and must convert it to PEM format or similar
Nothing wrong with doing that, but it is not technically required. The key information can be loaded either from a PEM file OR directly from the XML.
Option 1: Load Key from XML:
Parse the sample XML string into an object. Then extract the modulus and private exponent (ie the <D> element). Use the modulus and exponent to create a RSAPrivateKeySpec and load the RSA private key:
xmlKeyString = "<RSAKeyValue><Modulus>........</D></RSAKeyValue>";
xmlDoc = xmlParse(xmlKeyString);
modBytes = binaryDecode(xmlDoc.RSAKeyValue.Modulus.xmlText, "base64");
dBytes = binaryDecode(xmlDoc.RSAKeyValue.D.xmlText, "base64");
modulus = createObject("java","java.math.BigInteger").init(1, modBytes);
exponent = createObject("java","java.math.BigInteger").init(1, dBytes);
keySpec = createObject("java", "java.security.spec.RSAPrivateKeySpec").init(modulus, exponent);
keyFactory = createObject("java", "java.security.KeyFactory").getInstance("RSA");
privateKey = keyFactory.generatePrivate(keySpec);
Option 2: Load Key from PEM file:
Read the PEM file into a variable. Remove the header/trailer ie, "---BEGIN/END RSA PRIVATE KEY-----". Then decode the base64 contents and load the private key using a KeyFactory:
rawKey = replace( pemContent, "-----BEGIN RSA PRIVATE KEY-----", "" );
rawKey = replace( rawKey, "-----END RSA PRIVATE KEY-----", "" );
keyBytes = rawKey.trim().binaryDecode("base64");
keySpec = createObject("java", "java.security.spec.PKCS8EncodedKeySpec");
keyFactory = createObject("java", "java.security.KeyFactory").getInstance("RSA");
privateKey = keyFactory.generatePrivate(keySpec.init(keyBytes));
After you have loaded the private key, you can use a Signature object to perform the SHA1 hash and generate the signature with the RSA key:
stringToSign = "test#membersuite.com";
signer = createObject("java", "java.security.Signature").getInstance("SHA1withRSA");;
signer.initSign(privateKey);
signer.update( stringToSign.getBytes("us-ASCII"));
signedBytes = binaryEncode(signer.sign(), "base64");
writeDump(signedBytes);
Result (using sample XML):
jTDKoH+INi19kGWn7WRk/PZegLv/9fPUOluaM57x8y1tkuwxOiyX86gxsZ7gU/OsStIT9Q5SVSG5NoaL3B+AxjuLY8b7XBMfTXHv2vidrDkkTTBW0D2LsrkZ3xzmvvPqqfA3tF2HXUYF+zoiTsr3bQdA32CJ+lDNkf+QjV3ZEoc=
NB: Whichever method you choose, properly securing private keys is VERY important. Once you have the sample working, definitely read up on how to best store and secure private keys.

Related

Encrypted value length using Google KMS API in C# is different from the length of encrypted text generated directly from the API in postman

I am trying to encrypt the text in C# using google KeyManagementService.
I am not able to decrypt the cipher generated by KeyManagementServiceClient.Encrypt using KeyManagementServiceClient.Decrypt method. Decryption gives me:
Grpc.Core.RpcException: Status(StatusCode=InvalidArgument, Detail="Decryption failed: the ciphertext is invalid.")
I tried to use the google Try this API link to get the cipher and the length of the cipher generated by google API and the C# result are different.
Ex:
plaint text: text
cipher from C#:
TaRQSJ2KXrdmJJT6MmlD8RrcxzPJEa1jwAXWJ1puXg6nbl80aBcSLQBqSYOONfXhpZx8SyxCdB6mqTgr8uLJoAjva+Q4kN/p0+9RL2Sp2mHq4wjmZQ==
cipher from API:
TaRQSJ2KXv9ntnS7IszL077KNPtGJnqF9pSNiWANsq8gD0whezUSKwBqSYOOKKMifiWrfPDnHL5xETHPPlms0ztjkqa5hjdtkHwpzByLzi68A40
Has anyone phased the issue?
Here is my sample code
KeyManagementServiceClient keyManagementServiceClient =KeyManagementServiceClient.Create(channel);
byte[] plaintext=Encoding.ASCII.GetBytes("test");
EncryptRequest request = new EncryptRequest
{
CryptoKeyPathName = new CryptoKeyPathName("test-project", "global", "test-key", "encryption-key"),
Plaintext = ByteString.CopyFrom(plaintext),
};
EncryptResponse response =keyManagementServiceClient.Encrypt(request);
var cipher =
Convert.ToBase64String(response.Ciphertext.ToByteArray());
//Decrypt
ByteString ciphertext = ByteString.CopyFrom(Encoding.ASCII.GetBytes(cipher));
DecryptRequest req = new DecryptRequest
{
CryptoKeyName = new CryptoKeyName("test-project", "global", "test-key", "encryption-key"),
Ciphertext = ciphertext,
};
// Make the request
DecryptResponse res = keyManagementServiceClient.Decrypt(req);
You've base64-encoded the results of encrypting, but you haven't base64-decoded it before you try to decrypt it.
In the line
ByteString ciphertext = ByteString.CopyFrom(Encoding.ASCII.GetBytes(cipher));
It should instead look something like
ByteString ciphertext = ByteString.FromBase64(cipher);
(Note also that the ByteString class has built-in methods for going to/from Base64, if that's what you're looking to do.)

Decryption failed after registering AeadConfig more than once?

I'm encrypting some text and trying to decrypt it, however it's failing to decrypt when I am using a different AeadConfig.register() to decrypt than to encrypt. I'm wondering how to resolve this problem because I'll be encrypting on one activity and decrypting in another activity. I've simplified it for the sake of giving an example below.
This works, if I just pass the aead as a parameter, but I'm not sure if that's something I should be doing.
private fun deletableEncrypt() {
AeadConfig.register()
val keysetHandle: KeysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_GCM)
val aead: Aead = AeadFactory.getPrimitive(keysetHandle)
val plainText = "Hello world"
val aad = "masterpassword"
val cipherText = aead.encrypt(plainText.toByteArray(), aad.toByteArray())
Log.d(TAG, cipherText.toString())
dbHelper.insertNewRow("text", cipherText,
"moreText")
}
private fun deleteableDecrypt() {
AeadConfig.register()
val keysetHandle: KeysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_GCM)
val aead: Aead = AeadFactory.getPrimitive(keysetHandle)
val aad = "masterpassword"
val cipherText = dbHelper.getAllEncrypts()[0]
val decrypted = aead.decrypt(cipherText, aad.toByteArray())
}
Right now the decryption throws an exception. I think its' caused by the aeadConfig, but I don't know how to register it only once, so once this is solved it won't throw exception anymore.
I actually found the error, I'm generating a new KeySet everytime, when instead I should be storing it. Instructions on how to do that are found here https://github.com/google/tink/blob/master/docs/JAVA-HOWTO.md in the "Storing Keysets" section

.Net Support of SHA256withRSA

I need to support the following signature:
Sign the UTF-8 representation of the input using SHA256withRSA (also known as RSASSA-PKCS1-V1_5-SIGN with the SHA-256 hash function) with the private key obtained from the Google Developers Console. The output will be a byte array.
The following code fails, with "Invalid algorithem specified". Is this a limitation of .NET? Here is a snippet of my code:
var rsa2 = new RSAPKCS1SignatureFormatter(rsa);
rsa2.SetHashAlgorithm("SHA256");
bytes = rsa2.CreateSignature(bytes);
The above requirement is from computing the signature for a Server to Server Applications for Google API.
https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature
Thanks for any help.
Karl..
Here is the code that signs the JWT. I've removed the use of RSAPKC1SingatureFormatter class and using another HASHCompute method in an effort to get something to work (still not working)
I'm not sure this is correct and unfortunately my response from the Rest service is always the same "Invalid Grant" so hard to tell.
public string Generate(string HeadJWT, string ContentJWT, X509Certificate2 certificate)
{
var bytes = Utility.getBytes(HeadJWT);
var base64Head = Utility.Base64UrlEncode(bytes);
// base64 Url Encode Payload (Json Content)
bytes = Utility.getBytes(ContentJWT);
var base64Payload = Utility.Base64UrlEncode(bytes);
var secureInputValue = String.Format("{0}.{1}", base64Head, base64Payload);
bytes = Stub.Jwt.Utility.getBytes(secureInputValue);
bytes = Stub.Jwt.Utility.ComputeHMACSha265(bytes, certificate.PublicKey.EncodedKeyValue.RawData);
_signature = Stub.Jwt.Utility.Base64UrlEncode(bytes);
return String.Format("{0}.{1}.{2}", base64Head, base64Payload, _signature);
}
This cannot be a limitation of .NET in general, as the example Microsoft code seems to use "SHA256" itself. But it could be a limitation of your particular runtime.

SagePay AES form integration

I have a working Form Integration but need to move to v 3.00 of Form Integration which requires AES coding rather than Xor. The sample code is a J2EE app which hides away the method of encrypting. SagePay support have been unable to provide an API class/method for encrypting/decrypting; I suspect they are there, if anyone has used them and can advise on this?
Alternatively I am trying to do the encryption in my own code. Again SagePay have not been forthcoming, in that they don't provide enough information about how the encryption is done. I believe I have worked out that the password they provide is used both for the encryption key and iv, though they don't state this in the integration guide. Here is code I have tried for encoding inString
byte[] byteDataToEncrypt = inString.getBytes();
final byte[] keyBytes = webSite.encryptionPassword().getBytes("ASCII");
final byte[] ivBytes = webSite.encryptionPassword().getBytes("ASCII");
final SecretKey key = new SecretKeySpec(keyBytes, "AES");
final IvParameterSpec iv = new IvParameterSpec(ivBytes);
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] byteCipherText = cipher.doFinal(byteDataToEncrypt);
byte[] aesEncryptedBase64encoded = Base64.encodeBase64(byteCipherText);
String crypt = "#" + new String(aesEncryptedBase64encoded);
I do not know why # would need to be on the front, but I have seen it in other code that has been posted online for SagePay integration. Without it SagePay test server tells me the encryption method is not recognised. With it, it tells me the Currency field is missing, though I know the currency field is set ok to "GBP" in the source and I am using the provided test server password.
Any help very much appreciated whether from SagePay or from the developer community.
John
This works even if moving to ver4.00. The only thing that needs to be different for ver 4.00 is that it needs the encrypted string to be in upper case. Hence instead of
String crypt = "#" + Hex.encodeHexString(byteCipherText);
it will need to be
String crypt = "#" + Hex.encodeHexString(byteCipherText).toUpperCase();
Latest Sagepay/Opayo documentation at least verbally lists what all is needed and even if it is still not very comprehensive, together with this code, I could work it out.
Thanks.
The solution that worked for me is to replace this:
byte[] aesEncryptedBase64encoded = Base64.encodeBase64(byteCipherText);
String crypt = "#" + new String(aesEncryptedBase64encoded);
with this
String crypt = "#" + Hex.encodeHexString(byteCipherText);
or if using the SagePay API directly, replace all the code with just these two lines:
byte[] aesEncrypted = CryptographyHelper.AESEncrypt(detail, "ISO-8859-1", password);
String crypt = "#" + Hex.encodeHexString(aesEncrypted);

Unable to Generate HMac using Salesforce Crypto Class for Google Maps API

My Company recently signed up for Google Maps API for business.
To use the API, I need to generte a HMacSHA1 signature, and add it to my HTTP request. Unfortunately, somehow, I am not able to generate the right signature.
For testing, I am using the values provided by google to ensure that the algorithm works fine and I get the right result. Here is the code:
string url = 'maps/api/geocode/json?address=New+York&sensor=false&client=clientID';
string privateKey = 'vNIXE0xscrmjlyV-12Nj_BvUPaw=';
privateKey = privateKey.replace('-', '+');
privateKey = privateKey.replace('_', '/');
//Blob privateKeyBlob = EncodingUtil.base64Decode(privateKey);
Blob privateKeyBlob = Blob.valueOf(privateKey);
Blob urlBlob = Blob.valueOf(url);
Blob signatureBlob = Crypto.generateMac('HMacSHA1', urlBlob, privateKeyBlob);
String signature =EncodingUtil.urlEncode(EncodingUtil.base64Encode(signatureBlob), 'UTF-8');
signature = signature.replace('+', '-');
signature = signature.replace('/', '_');
system.debug('signature is ' +signature);
The generated signature should be : KrU1TzVQM7Ur0i8i7K3huiw3MsA=
Here is the link to Google Documentation where you can also find the same example: https://developers.google.com/maps/documentation/business/webservices
Few points to note:
1. I used the sample Python script provided in API Documentation and it gives the right result.
2. I think the problem is, API Documentation says that we should decode the privateKey and then provide it to the function. Although the documentation for Crypto Class says the "The value of privateKey does not need to be in decoded form.". I tried both, with and without decoding, still no result.
3. For Google API, everything has to be UTF-8 Encoded; I don't know if thats the way Encoding.Util decode's it.
I have tried a lot fo combinations, but could not find a solution. Any help would be highly appreciated.
Thanks,
Ankit
You need to sign the full path and query. Your string url is missing the leading slash (/).
Also, don't be afraid to open a support case with Google for this type of query.
We have used the code, but there are some defects in it, for whomever intend to use this code, the actual code will be:
string url = '/maps/api/geocode/json?address=New+York&sensor=false&client=clientID';
string privateKey = 'vNIXE0xscrmjlyV-12Nj_BvUPaw=';
privateKey = privateKey.replace('-', '+');
privateKey = privateKey.replace('_', '/');
Blob privateKeyBlob = EncodingUtil.base64Decode(privateKey);
Blob urlBlob = Blob.valueOf(url);
Blob signatureBlob = Crypto.generateMac('hmacSHA1', urlBlob, privateKeyBlob);
String signature = EncodingUtil.base64Encode(signatureBlob);
signature = signature.replace('+', '-');
signature = signature.replace('/', '_');
system.debug('*** Signature: ' + signature);

Resources