I'm implementing a credit card payment method form a provider (cardinity) that doesn't provide a .net implementation. I'm looking for similar stuff and end-up writing my own as my google skills seem to be ....
What I need is the base64 string of javax.crypto.mac
I am supporting the following methods:
enum EncryptionMethods
{
None=0,
HMACSHA1,
HMACSHA256,
HMACSHA384,
HMACSHA512,
HMACMD5
}
I have implemented the code you have above, the SecretKeySpec and the Mac the following way (you need System.Security.Cryptography.ProtectedData):
internal class Protected
{
private Byte[] salt = Guid.NewGuid().ToByteArray();
protected byte[] Protect(byte[] data)
{
try
{
return ProtectedData.Protect(data, salt, DataProtectionScope.CurrentUser);
}
catch (CryptographicException)//no reason for hackers to know it failed
{
return null;
}
}
protected byte[] Unprotect(byte[] data)
{
try
{
return ProtectedData.Unprotect(data, salt, DataProtectionScope.CurrentUser);
}
catch (CryptographicException)//no reason for hackers to know it failed
{
return null;
}
}
}
internal class SecretKeySpec:Protected,IDisposable
{
readonly EncryptionMethods _method;
private byte[] _secretKey;
public SecretKeySpec(byte[] secretKey, EncryptionMethods encryptionMethod)
{
_secretKey = Protect(secretKey);
_method = encryptionMethod;
}
public EncryptionMethods Method => _method;
public byte[] SecretKey => Unprotect( _secretKey);
public void Dispose()
{
if (_secretKey == null)
return;
//overwrite array memory
for (int i = 0; i < _secretKey.Length; i++)
{
_secretKey[i] = 0;
}
//set-null
_secretKey = null;
}
~SecretKeySpec()
{
Dispose();
}
}
internal class Mac : Protected,IDisposable
{
byte[] rawHmac;
HMAC mac;
public Mac(SecretKeySpec key, string data)
{
switch (key.Method)
{
case EncryptionMethods.HMACMD5:
mac = new HMACMD5(key.SecretKey);
break;
case EncryptionMethods.HMACSHA512:
mac = new HMACSHA512(key.SecretKey);
break;
case EncryptionMethods.HMACSHA384:
mac = new HMACSHA384(key.SecretKey);
break;
case EncryptionMethods.HMACSHA256:
mac = new HMACSHA256(key.SecretKey);
break;
case EncryptionMethods.HMACSHA1:
mac = new HMACSHA1(key.SecretKey);
break;
default:
throw new NotSupportedException("not supported HMAC");
}
rawHmac = Protect( mac.ComputeHash(Cardinity.ENCODING.GetBytes(data)));
}
public string AsBase64()
{
return System.Convert.ToBase64String(Unprotect(rawHmac));
}
public void Dispose()
{
if (rawHmac != null)
{
//overwrite memory address
for (int i = 0; i < rawHmac.Length; i++)
{
rawHmac[i] = 0;
}
//release memory now
rawHmac = null;
}
mac?.Dispose();
mac = null;
}
~Mac()
{
Dispose();
}
}
I have implemented this in an OAuthSigner class the following way:
public override string ComputeSignature(string plainTextToEncode, string consumerSecret)
{
var key = PercentEncode(consumerSecret) + "&";
try
{
using (var secretKey = new SecretKeySpec(key.GetBytes(), EncryptionMethods.HMACSHA1))
using (Mac mac = new Mac(secretKey, plainTextToEncode))
{
return mac.AsBase64();
}
}
finally
{
key = null;//free memory, remove sensitive data
}
}
Then, it's not what you ask for but I need a helper method as I am sending my text to a web service that goes like this and I include it as some might copy the code:
public static String PercentEncode(string textToEncode)
{
return string.IsNullOrEmpty(textToEncode)
?""
: UrlEncoder.Default.Encode(Cardinity.ENCODING.GetString(Cardinity.ENCODING.GetBytes(textToEncode)))
.Replace("+", "%20").Replace("*", "%2A")
.Replace("%7E", "~");
}
The class UrlEncoder comes from System.Text.Encodings.Web, you may have to add a reference.
The class named Cardinity implements a "short-cut" to the Encoding that I use for Cardinity
public abstract class Cardinity
{
...
public static String API_BASE = "https://api.cardinity.com";
public static String API_VERSION = "v1";
public static String VERSION = "0.1";
public static String ENCODING_CHARSET = "UTF-8";
public static Encoding ENCODING => Encoding.UTF8;
}
as Java uses string.GetBytes a lot, I have added an extension method for this that I call above in the key.GetBytes(), here is the extension code:
public static byte[] GetBytes(this string sender)=>
Cardinity.ENCODING.GetBytes(sender);
My test method, I have copied the values from Cardinity API passes without any issues.
private OAuthSigner signer;
public HmacOAuthSigner_Test()
{
signer = new HmacOAuthSigner();
}
[TestMethod]
public void Test_HmacOAuthSigner_ComputeSignature_DefaultText()
{
var expects = "PxkffxyQh6jsDNcgJ23GpAxs2y8=";
var test_data = "justsomerandommessage";
var secretkey = "yvp0leodf231ihv9u29uuq6w8o4cat9qz2nkvs55oeu833s621";
var actual = signer.ComputeSignature(test_data, secretkey);
Assert.AreEqual(expects, actual, $"Expecting {test_data} to return {expects} received {actual}");
}
The whole implementation of the HmacOAuthSigner is here, it implements an abstract class with the PercentEncode method in it.
public class HmacOAuthSigner : OAuthSigner
{
public override string ComputeSignature(string signatureBaseString, string consumerSecret)
{
var key = PercentEncode(consumerSecret) + "&";
var secretKey = new SecretKeySpec(key.GetBytes(), EncryptionMethods.HMACSHA1);
using (Mac mac = new Mac(secretKey, signatureBaseString))
{
return mac.AsBase64();
}
}
public override string GetSignatureMethod()
{
return "HMAC-SHA1";
}
}
and the abstract class that I use as a contract for all the implementations:
public abstract class OAuthSigner
{
/// <summary>
/// Signature method used
/// </summary>
/// <returns>a string that tells the implementation method</returns>
public abstract string GetSignatureMethod();
/// <summary>
/// computes the signature that is used with the encryption based on the keys provided by cardinity
/// </summary>
/// <param name="signatureBaseString">The secret string that services as a base</param>
/// <param name="consumerSecret">The consumer key as specified in the API settings</param>
/// <returns>signature string computed by the provided parameters using the signature method</returns>
public abstract string ComputeSignature(String signatureBaseString, String consumerSecret);
/// <summary>
/// Encode a string into a format expected by Cardinity
/// </summary>
/// <param name="textToEncode">The text that is to be encoded</param>
/// <returns>web encoded string ready for using to send to Cardinity</returns>
public static String PercentEncode(string textToEncode)
{
return string.IsNullOrEmpty(textToEncode)
?""
: UrlEncoder.Default.Encode(Cardinity.ENCODING.GetString(Cardinity.ENCODING.GetBytes(textToEncode)))
.Replace("+", "%20").Replace("*", "%2A")
.Replace("%7E", "~");
}
}