C# SDK Sample: Hosted Fields (Credit Card Numbers)

PemKeyUtils is used by the Hosted Fields to handle RSA encryption

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace SdkSample {
  public class PemKeyUtils {
        
    public static RSACryptoServiceProvider GetRsaProviderFromPublicKey( string pemString )
    {
        var pemkey = DecodeOpenSslPublicKey( pemString );

        return pemkey == null ? null : DecodeX509PublicKey( pemkey );

    }



    //--------   Get the binary RSA PUBLIC key   --------
      private static byte[] DecodeOpenSslPublicKey( string instr )
    {
        const string pempubheader = "-----BEGIN PUBLIC KEY-----";
        const string pempubfooter = "-----END PUBLIC KEY-----";
        string pemString = instr.Trim();
        byte[] binkey;
        if (!pemString.StartsWith( pempubheader ) || !pemString.EndsWith( pempubfooter ))
            return null;
        StringBuilder sb = new StringBuilder( pemString );
        sb.Replace( pempubheader, "" );  //remove headers/footers, if present
        sb.Replace( pempubfooter, "" );

        string pubstr = sb.ToString().Trim();   //get string after removing leading/trailing whitespace

        try
        {
            binkey = Convert.FromBase64String( pubstr );
        }
        catch (FormatException)
        {       //if can't b64 decode, data is not valid
            return null;
        }
        return binkey;
    }

    static RSACryptoServiceProvider DecodeX509PublicKey(byte[] x509Key)
    {
        // encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
        byte[] seqOid = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
        // ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
        using (var mem = new MemoryStream(x509Key))
        {
            using (var binr = new BinaryReader(mem))    //wrap Memory Stream with BinaryReader for easy reading
            {
                try
                {
                    var twobytes = binr.ReadUInt16();
                    switch (twobytes)
                    {
                        case 0x8130:
                            binr.ReadByte();    //advance 1 byte
                            break;
                        case 0x8230:
                            binr.ReadInt16();   //advance 2 bytes
                            break;
                        default:
                            return null;
                    }

                    var seq = binr.ReadBytes(15);
                    if (!CompareByteArrays(seq, seqOid))  //make sure Sequence for OID is correct
                        return null;

                    twobytes = binr.ReadUInt16();
                    if (twobytes == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81)
                        binr.ReadByte();    //advance 1 byte
                    else if (twobytes == 0x8203)
                        binr.ReadInt16();   //advance 2 bytes
                    else
                        return null;

                    var bt = binr.ReadByte();
                    if (bt != 0x00)     //expect null byte next
                        return null;

                    twobytes = binr.ReadUInt16();
                    if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                        binr.ReadByte();    //advance 1 byte
                    else if (twobytes == 0x8230)
                        binr.ReadInt16();   //advance 2 bytes
                    else
                        return null;

                    twobytes = binr.ReadUInt16();
                    byte lowbyte;
                    byte highbyte = 0x00;

                    if (twobytes == 0x8102) //data read as little endian order (actual data order for Integer is 02 81)
                        lowbyte = binr.ReadByte();  // read next bytes which is bytes in modulus
                    else if (twobytes == 0x8202)
                    {
                        highbyte = binr.ReadByte(); //advance 2 bytes
                        lowbyte = binr.ReadByte();
                    }
                    else
                        return null;
                    byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };   //reverse byte order since asn.1 key uses big endian order
                    int modsize = BitConverter.ToInt32(modint, 0);

                    byte firstbyte = binr.ReadByte();
                    binr.BaseStream.Seek(-1, SeekOrigin.Current);

                    if (firstbyte == 0x00)
                    {   //if first byte (highest order) of modulus is zero, don't include it
                        binr.ReadByte();    //skip this null byte
                        modsize -= 1;   //reduce modulus buffer size by 1
                    }

                    byte[] modulus = binr.ReadBytes(modsize); //read the modulus bytes

                    if (binr.ReadByte() != 0x02)            //expect an Integer for the exponent data
                        return null;
                    int expbytes = binr.ReadByte();        // should only need one byte for actual exponent data (for all useful values)
                    byte[] exponent = binr.ReadBytes(expbytes);

                    // We don't really need to print anything but if we insist to...
                    //showBytes("\nExponent", exponent);
                    //showBytes("\nModulus", modulus);

                    // ------- create RSACryptoServiceProvider instance and initialize with public key -----
                    RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
                    RSAParameters rsaKeyInfo = new RSAParameters
                    {
                        Modulus = modulus,
                        Exponent = exponent
                    };
                    rsa.ImportParameters(rsaKeyInfo);
                    return rsa;
                }
                catch (Exception)
                {
                    return null;
                }
            }
        }
    }


      private static bool CompareByteArrays( byte[] a, byte[] b )
    {
        if (a.Length != b.Length)
            return false;
        var i = 0;
        foreach (var c in a)
        {
            if (c != b[i])
                return false;
            i++;
        }
        return true;
    }
   
    
  }
}


using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Security.Cryptography;
using System.Text;

namespace SdkSample {
  public class HostedFields {

    private const string BaseAddress = "https://token.ultracart.com";
    private const string HostedFieldsPath = "/cgi-bin/UCCheckoutAPIHostedFields";
    private const string PublicKeyPath = "/cgi-bin/UCCheckoutAPIHostedFieldsPublicKey";
    private const string Referrer = "https://token.ultracart.com/";
    private static readonly Uri ReferrerUri = new Uri(Referrer);
    private const string Version = "1.0";

    // ReSharper disable once InconsistentNaming
    private const string OperationCVV = "storeCreditCardCvv2";

    // ReSharper disable once InconsistentNaming
    private const string OperationCC = "storeCreditCardNumber";


    public string MerchantId { get; set; }

    public string CartId { get; set; }

    //public string PublicKey { get; set; }
    public RSACryptoServiceProvider Rsa { get; set; }


    public static HostedFields Create(string merchantId) {
      return Create(merchantId, null);
    }


    public static HostedFields Create(string merchantId, string cartId) {
      var hostedFields =
        new HostedFields(merchantId, cartId) {Rsa = PemKeyUtils.GetRsaProviderFromPublicKey(GetPublicKey())};

      return hostedFields;
    }


    private HostedFields(string merchantId, string cartId) {
      MerchantId = merchantId;
      CartId = cartId;
    }


    private static string GetPublicKey() {

      var hc = new HttpClient {BaseAddress = new Uri(BaseAddress)};
      var result = hc.GetAsync(PublicKeyPath).Result;
      var key = result.Content.ReadAsStringAsync().Result;
      Console.WriteLine(key);
      return key;
    }


    private static JsonResult PostToTokenVault(HttpContent payload) {

      var hc = new HttpClient {BaseAddress = new Uri(BaseAddress)};

      hc.DefaultRequestHeaders.Referrer = ReferrerUri;
      var result = hc.PostAsync(HostedFieldsPath, payload).Result;

      var serializer = new DataContractJsonSerializer(typeof(JsonResult));
      var jsonResult = (JsonResult) serializer.ReadObject(result.Content.ReadAsStreamAsync().Result);

      Console.WriteLine($"PostJsonPayload Result: {jsonResult}");

      return jsonResult;
    }


    public JsonResult StoreNumber(string creditCardNumber) {

      Console.WriteLine($"Storing CC: {creditCardNumber}");

      var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
      Console.WriteLine($"Epoch: {epoch}");

      var timestamp = (long) (DateTime.UtcNow - epoch).TotalMilliseconds;
      Console.WriteLine($"Timestamp: {timestamp}");

      var unencryptedPayload = creditCardNumber + "|" + timestamp;
      Console.WriteLine($"Unencrypted Payload: {unencryptedPayload}");

      var encryptedBytes = Rsa.Encrypt(Encoding.ASCII.GetBytes(unencryptedPayload), false);
      //Console.WriteLine("Encrypted: " + Encoding.Default.GetString(encryptedBytes));

      var base64Payload = Convert.ToBase64String(encryptedBytes);
      Console.WriteLine("Base 64:");
      Console.WriteLine(base64Payload);

      var content = new List<KeyValuePair<string, string>>() {
        new KeyValuePair<string, string>("merchantId", MerchantId),
        new KeyValuePair<string, string>("operation", OperationCC),
        new KeyValuePair<string, string>("version", Version),
        new KeyValuePair<string, string>("creditCardNumberEncrypted", base64Payload),
        new KeyValuePair<string, string>("referrer", Referrer),
      };
      // if the card number belongs to a shopping cart, cart id should be provided.  if the card number will be used
      // to update an existing order, and auto order, and a customer profile (card already on file), then do not
      // provide a cart id and a token will be returned.  That token is then attached to the appropriate record and
      // the server side logic will retrieve the credit card from the token vault using the token as the key.
      if (CartId != null) {
        content.Add(new KeyValuePair<string, string>("shoppingCartId", CartId));
      }

      var formContent = new FormUrlEncodedContent(content.ToArray());


      return PostToTokenVault(formContent);

    }


    // ReSharper disable once InconsistentNaming
    public JsonResult StoreCVV(string cvv) {
      var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
      var timestamp = (long) (DateTime.UtcNow - epoch).TotalMilliseconds;
      var unencryptedPayload = cvv + "|" + timestamp;


      var encryptedBytes = Rsa.Encrypt(Encoding.ASCII.GetBytes(unencryptedPayload), false);
      var base64Payload = Convert.ToBase64String(encryptedBytes);

      var content = new List<KeyValuePair<string, string>>() {
        new KeyValuePair<string, string>("merchantId", MerchantId),
        new KeyValuePair<string, string>("operation", OperationCVV),
        new KeyValuePair<string, string>("version", Version),
        new KeyValuePair<string, string>("creditCardCvv2Encrypted", base64Payload),
        new KeyValuePair<string, string>("referrer", Referrer),
      };
      // if the card number belongs to a shopping cart, cart id should be provided.  if the card number will be used
      // to update an existing order, and auto order, and a customer profile (card already on file), then do not
      // provide a cart id and a token will be returned.  That token is then attached to the appropriate record and
      // the server side logic will retrieve the credit card from the token vault using the token as the key.
      if (CartId != null) {
        content.Add(new KeyValuePair<string, string>("shoppingCartId", CartId));
      }

      var formContent = new FormUrlEncodedContent(content.ToArray());

      return PostToTokenVault(formContent);

    }


    [DataContract]
    [SuppressMessage("ReSharper", "InconsistentNaming")]
    public class JsonResult {
      [DataMember] internal bool success;

      [DataMember] internal string maskedValue;

      [DataMember] internal string cardType;

      [DataMember] internal string token;

      [DataMember] internal string errorMessage;


      public override string ToString() {
        return
          $"{nameof(success)}: {success}, {nameof(maskedValue)}: {maskedValue}, {nameof(cardType)}: {cardType}, {nameof(token)}: {token}, {nameof(errorMessage)}: {errorMessage}";
      }

    }


//    private static int Main() {
//
//      Console.WriteLine("HostedFields.Main executing.");
//
//      const string merchantId = "DEMO";
//      const string cartId = "1234567890";
//
//      var hostedFields = Create(merchantId, cartId);
//
//
//      var ccResult = hostedFields.StoreNumber("4444333322221111");
//      Console.WriteLine("Result of Storing CC Number:");
//      Console.WriteLine(ccResult);
//
//      var cvvResult = hostedFields.StoreCVV("123");
//      Console.WriteLine("Result of Storing CVV Number:");
//      Console.WriteLine(cvvResult);
//
//      return 0;
//    }


  }
}