4
\$\begingroup\$

In my experience i had the need that some application (both desktop and web) can excange data to authenticate users (even if the data for authenticating users are not reacheable form desktop application).

The simple solution i came up to, was to generate some kind of encrypted information to distribute to users, and the user give me back when need to authenticate to application (eg : Cookie, Rest API parameter, QR Code).

Now that i have few spare time, i choose to develop a little library that will sum up my previous expirience with those "token like" solution; in a more organized and consistent way. In this way, as long as the applications involved share the master key, the token can be decrypted and validated (to see if the user that give it to me was an authenticated one) by any of the parties involved.

For the reason i mentioned before, this code is "brand new" (never used in production nor planned to be used). But anyway i would like to understand if it contains any mistake, more for a didactic pourpose than for a pratical reason.

I am particularry concerned about :

  • Design of the factory with the Delegate structure to access SecureString object
  • Possible bug in the code
  • Security hole in encryption and "security scheme"

Here the reference to the repository with all the code : https://github.com/ItSkary/TokenGenerator

Here an example of library usage :

static void Main(string[] args)
{
    SecureString masterKey = new SecureString();
    masterKey.AppendChar('M');
    masterKey.AppendChar('a');
    masterKey.AppendChar('s');
    masterKey.AppendChar('t');
    masterKey.AppendChar('e');
    masterKey.AppendChar('r');
    masterKey.AppendChar('K');
    masterKey.AppendChar('e');
    masterKey.AppendChar('y');

    //Setup the facotry with a specific master key (opionally setting up token default timout and data size) 
    GenericTokenFactory.Setup(masterKey, tokenSize : 150, timeoutMs: 20_000);

    //After setup we can obtain a factory instance
    var factory = GenericTokenFactory.Instance();

    //With the factory we can build as many token as reuquired
    var token1 = factory.Create();
    var token2 = factory.Create();

    //foreach token we can attach information, infomration must be strings
    var @dynToken1 = token1.ToDynamic;
    @dynToken1["UserName"] = "CurrentUser";
    @dynToken1.OtherData = "OtherData";
    @dynToken1.DecimalValue = 3.44567.AsInvariantString();

    //check if data persisted in the token fit the token size (token have predefined fixed size to not disclose any hint about the content)
    Console.WriteLine("Are boundaries valid : " + token1.CheckBoundaries().ToString());

    //unencrypted token representation
    Console.WriteLine("Plaintext of token1       : " + token1.ToString());

    //deep clone a token from another
    var token1Clone = token1.Clone();
    token1Clone.ToDynamic.OtherData = "NewValue";

    //unencrypted token representation
    string unencryptedTokenData = token1Clone.ToString();
    Console.WriteLine("Plaintext of token1 clone : " + unencryptedTokenData);

    //encrypt token data before distributing it
    string encryptedToken = token1.Encrypt();
    Console.WriteLine("Encrypted token : " + encryptedToken + "\r\n");

    //get a token back when encrypted (when received back after distribution)
    var decryptedToken = factory.Decrypt(encryptedToken);

    //get back a token from its string representation in plaintext
    var token = factory.Create(unencryptedTokenData); //similar to colne operation, usefull if token was persisted in plaintext before distribution (do not use it for cloning pourpose)

    //check if the token exist time has exceeded timout, check is made against a custom timeout supplied of one minute.
    //if no data is supplied to the method the timeout of reference is the value passed in the setup phase (20s)
    Console.WriteLine("Is expired : " + token.IsExpired(timeoutMs: 60_000).ToString());

    //try to istantiate a token from invalid data 
    Console.WriteLine("Try to instantiate Token from invalid data");//plaintext is used, same happened from encrypted text if invalid
    try
    {
        var invalidToken = factory.Create("Some invalid, not token like, data");
    }
    catch (Exception err )
    {
        Console.WriteLine("Creation failed : " + err.Message);
    }

    Console.WriteLine("");
    Console.WriteLine("Type a key to contienue..");
    Console.ReadKey();
}

--- Edited : adding code here (even if a bit long)

Factory Class

using SecurityDriven.Inferno;
using SecurityDriven.Inferno.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;

namespace Token
{
    public class GenericTokenFactory
    {
        /// <summary>
        /// A pepper value to bind keys to this program : https://en.wikipedia.org/wiki/Pepper_(cryptography)
        /// </summary>
        private static readonly byte[] _PEPPER = new byte[] { 0x78, 0x64, 0x70, 0x71, 0x36, 0x73, 0x7a, 0x63, 0x55, 0x50, 0x73, 0x32, 0x61, 0x36, 0x65, 0x4b, 0x4d, 0x66, 0x38, 0x37, 0x41 };

        private static object _sync = new object();
        private static GenericTokenFactory _instance = null;

        private static bool _setupHappened = false;
        private static int _tokenSize = 0;
        private static int _timeoutMs = 600_000; //10 minutes 
        private static SecureString _masterKey = null;

        /// <summary>
        /// Setup configuration to get instance of GenericTokenFactory
        /// </summary>
        /// <param name="masterKey">A SecureString used to store masterKey</param>
        /// <param name="tokenSize">The configuration to set current token size (all token have fixed size regardeless of the content) (optional : default=128)</param>
        /// <param name="timeoutMs">The configuration to set current token timeout expressed in ms (optional : default=600000 equal to 10 miuntes)</param>
        /// <remarks>Setup method is applied only once</remarks>
        /// <return>True if the data are applied, false otherwise (data are ignored if a previous setup already happened)</return>
        public static bool Setup ( SecureString masterKey, int tokenSize = 0, int timeoutMs = 600_000)
        {
            if (_setupHappened)
                return false;

            lock (_sync)
            {
                if (_setupHappened)
                    return false;

                _tokenSize = tokenSize;
                _timeoutMs = timeoutMs;
                _masterKey = GenericTokenFactory.AccessSecuredData<SecureString>(masterKey, _deriveKeyDelegateRef);
                _setupHappened = true;

                return true;
            }
        }

        /// <summary>
        /// Create or get an instance of GenericTokenFactory
        /// </summary>
        /// <returns>An instance of GenericTokenFactory</returns>
        public static GenericTokenFactory Instance()
        {
            if ( _setupHappened == false)
                throw new InvalidOperationException("Execute setup operation first");

            if (_instance != null)
                return _instance;

            lock (_sync)
            {
                if (_instance != null)
                    return _instance;

                if (_masterKey == null)
                    throw new InvalidOperationException("Setup the MasterKey first");

                _instance = new GenericTokenFactory();
                return _instance;
            }
        }

        private GenericTokenFactory(){}

        /// <summary>
        /// Create a new instance of GnericToken
        /// </summary>
        /// <returns>The new instnace of GnericToken</returns>
        public GenericToken Create ()
        {
            return new GenericToken(this, size : _tokenSize, timeoutMs : _timeoutMs);
        }

        /// <summary>
        /// Create a new instance of GenericTokne starting from its string representation (in plain text)
        /// </summary>
        /// <param name="plainTextTokenData"></param>
        /// <returns></returns>
        public GenericToken Create(string plainTextTokenData)
        {
            return new GenericToken(this, tokenData: plainTextTokenData);
        }

        /// <summary>
        /// Decrypt the supplid string (representing an encrypted token) and return the GenericToken represented
        /// </summary>
        /// <param name="encryptedTokenData">Encrypted string that represent a GenericToken</param>
        /// <returns>The GenericToken represented by encrypted string</returns>
        public GenericToken Decrypt (string encryptedTokenData)
        {
            return this.Create (GenericTokenFactory.AccessSecuredData<string>(_masterKey,_decryptDelegateRef, encryptedTokenData));
        }

        /// <summary>
        /// Encrypt the supplied GenericToken
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        internal string Encrypt ( GenericToken token)
        {
            return GenericTokenFactory.AccessSecuredData<string>(_masterKey, _encryptDelegateRef, token.ToString());
        }



        /// <summary>
        /// Execute the <paramref name="operationDelegate"/> provided passing the decoded <paramref name="secureString"/> as first argument of type byte array
        /// </summary>
        /// <typeparam name="TReturn">The delegate return type</typeparam>
        /// <param name="secureString">The SecureString to decode</param>
        /// <param name="operationDelegate">The delegate with the logic to execute</param>
        /// <param name="parameters">The parameter to povide to the <paramref name="operationDelegate"/></param>
        /// <returns>The return value of the delegate</returns>
        /// <remarks>The <paramref name="operationDelegate"/> has to take a first parameter of type byte array</remarks>
        private static TReturn AccessSecuredData<TReturn> (SecureString secureString, Delegate operationDelegate , params object[] parameters)
        {
            var pUnicodeBytes = Marshal.SecureStringToGlobalAllocUnicode(secureString);
            byte[] workArray = null;
            try
            {
                workArray = new byte[secureString.Length * 2];

                for (var idx = 0; idx < workArray.Length; ++idx)
                {
                    workArray[idx] = Marshal.ReadByte(pUnicodeBytes, idx);
                }

                List<object> actualParameters = new List<object>(parameters ?? new object[0]);
                actualParameters.Insert(0, workArray);

                return (TReturn)operationDelegate.DynamicInvoke(actualParameters.ToArray());
            }
            finally
            {
                if (workArray != null)
                    for (int i = 0; i < workArray.Length; i++)
                        workArray[i] = 0;

                Marshal.ZeroFreeGlobalAllocUnicode(pUnicodeBytes);
            }
        }


        /// <summary>
        /// Delegate to represent decrypt operation
        /// </summary>
        /// <param name="key">The key to use to dencrypt data</param>
        /// <param name="data">The encrypted text</param>
        /// <returns></returns>
        private delegate string DecryptDelegate(byte[] orignalKey, string data);
        /// <summary>
        /// Delegate reference to Decrypt method
        /// </summary>
        private static Delegate _decryptDelegateRef = new DecryptDelegate(Decrypt);
        /// <summary>
        /// Decrypt the token data with the current key
        /// </summary>
        /// <param name="key">The key to use to decrypt data</param>
        /// <param name="data">The encrypted text</param>
        /// <returns></returns>
        private static string Decrypt(byte[] key, string data)
        {
            return SuiteB.Decrypt(key, data.FromB64().AsArraySegment()).FromBytes();
        }


        /// <summary>
        /// Delegate to represent encrypt operation
        /// </summary>
        /// <param name="key">The key to use to encrypt data</param>
        /// <param name="data">The plain text</param>
        /// <returns></returns>
        private delegate string EncryptDelegate(byte[] derivedKey, string data);
        /// <summary>
        /// Delegate reference to Encrypt method
        /// </summary>
        private static Delegate _encryptDelegateRef = new EncryptDelegate(Encrypt);
        /// <summary>
        /// Encrypt the token data with the current key
        /// </summary>
        /// <param name="derivedKey">The key to use to encrypt data</param>
        /// <param name="data">The plain text</param>
        /// <returns></returns>
        private static string Encrypt ( byte[] derivedKey, string data )
        {
            var byteArray = SuiteB.Encrypt(derivedKey, data.ToBytes().AsArraySegment());
            return byteArray.ToB64();
        }


        /// <summary>
        /// Delegate to represent key derivation operation
        /// </summary>
        /// <param name="orignalKey"></param>
        /// <returns></returns>
        private delegate SecureString DeriveKeyDelegate(byte[] orignalKey);
        /// <summary>
        /// Delegate reference to DeriveKey method
        /// </summary>
        private static Delegate _deriveKeyDelegateRef = new DeriveKeyDelegate(DeriveKey);
        /// <summary>
        /// Derive the current key from the master key. 
        /// </summary>
        /// <param name="originalKey">The original master key</param>
        /// <returns>A SecureString containing the derived key</returns>
        private static SecureString DeriveKey ( byte[] originalKey )
        {
            //TODO : evaluate a way to make the derived key a bit more ephimeral so that can be changed frequently without affecting already generated token
            using (var hmac = SuiteB.HmacFactory())
            {
                hmac.Key = originalKey;
                byte [] derived = hmac.ComputeHash(_PEPPER) ;

                SecureString result = new SecureString();
                foreach (byte character in derived)
                    result.AppendChar((char)character);

                return result;
            }
        }

    }
}

Token Class :

using SecurityDriven.Inferno;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;

namespace Token
{
    public class GenericToken : DynamicObject
    {
        private const int _DEF_SIZE = 128;
        private const int _DEF_EXPIRATION_MS = 36_000_000; // 10h
        private const char _NAME_VALUE_SEPARATOR = '␝';
        private const char _ITEMS_SEPARATOR = '␜';
        private const string _DEF_KEY = "__";
        private const string _DEF_KEY_VAL = "standard";
        private const string _SYS_KEY_SIZE = "__size";
        private const string _SYS_KEY_CREATION_TIME = "__creationTime";
        private const string _SYS_KEY_TIMEOUTMS = "__timeoutMs";

        private static readonly string[] _SYS_KEYS = new string[] { _DEF_KEY, _SYS_KEY_SIZE, _SYS_KEY_CREATION_TIME, _SYS_KEY_TIMEOUTMS };

        private GenericTokenFactory _factoryRef = null;
        private ConcurrentDictionary<string, string> _properties = new ConcurrentDictionary<string, string>();

        public dynamic ToDynamic { get { return (dynamic)this; } }

        /// <summary>
        /// Private constructor used for cloning operation
        /// </summary>
        /// <param name="factory">Factory reference form instance to clone</param>
        /// <param name="properties">Properties of the instance to clone</param>
        private GenericToken(GenericTokenFactory factory, ConcurrentDictionary<string, string> properties)
        {
            if (factory == null)
                throw new ArgumentNullException("factory");

            if (properties == null)
                throw new ArgumentNullException("properties");

            _factoryRef = factory;

            foreach (string key in properties.Keys)
                if (_properties.ContainsKey(key) == false)
                    _properties.TryAdd(key, properties[key]);
        }

        /// <summary>
        /// Class constructor
        /// </summary>
        /// <param name="factory">Reference to the factory that build that instance</param>
        /// <param name="size">Size of the current instance when represented as string</param>
        /// <param name="timeoutMs">Size of the current instance when represented as string</param>
        internal GenericToken(GenericTokenFactory factory, int size = 0, int timeoutMs = _DEF_EXPIRATION_MS)
        {
            if (factory == null)
                throw new ArgumentNullException("factory");

            if (timeoutMs <= 0)
                timeoutMs = _DEF_EXPIRATION_MS;

            _factoryRef = factory;

            if (size <= 0)
                size = _DEF_SIZE;

            //can not be assigned thorugh dynamic access because this property is read-only
            _properties[_DEF_KEY]               = _DEF_KEY_VAL; 
            _properties[_SYS_KEY_SIZE]          = size.AsInvariantString();
            _properties[_SYS_KEY_CREATION_TIME] = DateTime.Now.Ticks.AsInvariantString();
            _properties[_SYS_KEY_TIMEOUTMS]     = timeoutMs.AsInvariantString();
        }

        #region Not Supported Methods
        public override bool TryDeleteIndex(DeleteIndexBinder binder, object[] indexes)
        {
            throw new NotSupportedException();
        }

        public override bool TryDeleteMember(DeleteMemberBinder binder)
        {
            throw new NotSupportedException();
        }

        public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
        {
            throw new NotSupportedException();
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            throw new NotSupportedException();
        }

        public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result)
        {
            throw new NotSupportedException();
        }

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            throw new NotSupportedException();
        }

        public override bool TryCreateInstance(CreateInstanceBinder binder, object[] args, out object result)
        {
            throw new NotSupportedException();
        }

        public override bool TryUnaryOperation(UnaryOperationBinder binder, out object result)
        {
            throw new NotSupportedException();
        }
        #endregion

        /// <summary>
        /// Create a new instance starting from an existing token representation as tring
        /// </summary>
        /// <param name="factory">Reference to the factory that build that instance</param>
        /// <param name="tokenData">The plaintext representation of data inside the current token</param>
        internal GenericToken(GenericTokenFactory factory, string tokenData)
        {
            if (factory == null)
                throw new ArgumentNullException("factory");

            if ( string.IsNullOrWhiteSpace(tokenData) )
                throw new ArgumentNullException("tokenData");

            if ( tokenData.IndexOf(_NAME_VALUE_SEPARATOR) <= 0|| tokenData.IndexOf(_ITEMS_SEPARATOR) <= 0)
                throw new ArgumentException("tokenData");

            if (_properties == null)
                throw new InvalidOperationException("Current object internal state is not valid");

            _factoryRef = factory;

            string[] keypairs = tokenData.Split(new char[] { _ITEMS_SEPARATOR }, StringSplitOptions.RemoveEmptyEntries);
            int keyPairCounter = 1;
            foreach (string keypair in keypairs)
            {
                int separatorIndex = keypair.IndexOf(_NAME_VALUE_SEPARATOR);

                if (separatorIndex <= 0 && keyPairCounter != keypairs.Length)
                    throw new ArgumentException("Invalid parameter in the data", "tokenData");
                else if (separatorIndex <= 0 && keyPairCounter == keypairs.Length)
                    break;

                string[] parts = keypair.Split(new char[] { _NAME_VALUE_SEPARATOR }, 2, StringSplitOptions.RemoveEmptyEntries);
                if (parts.Length != 2)
                    throw new ArgumentException("Invalid parameter in the data", "tokenData");

                string key   = parts[0];
                string value = parts[1];

                _properties.TryAdd(key, value);
                keyPairCounter++;
            }

            if (!_properties.ContainsKey(_DEF_KEY) ||
                 _properties[_DEF_KEY] != _DEF_KEY_VAL)
                throw new ArgumentException("Invalid parameter in the data", "tokenData");
        }

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return _properties?.Keys?.ToArray() ?? new string[0];
        }

        public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
        {
            if (indexes == null || indexes.Length <= 0) 
                throw new IndexOutOfRangeException();

            if (indexes[0].GetType() != typeof(int) && indexes[0].GetType() != typeof(string))
                throw new ArgumentException("indexes");

            if (_properties == null)
                throw new InvalidOperationException("Current object internal state is not valid");

            var index = indexes[0];
            string key = null;
            if ( index is int)
            {
                int indexAsInt = (int)index;
                if (indexAsInt < 0 || indexAsInt >= _properties.Keys.Count)
                    throw new IndexOutOfRangeException();

                key = _properties.ElementAt(indexAsInt).Key;
            }
            else if ( index is string)
            {
                key = (string)index;

                if (_properties.ContainsKey(key) == false)
                    result = null; ;
            }

            if (string.IsNullOrEmpty(key))
                throw new ArgumentException("indexes");

            result = _properties[key];

            return true;
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (string.IsNullOrEmpty(binder?.Name ?? string.Empty))
                throw new ArgumentException("binder.Name");

            if (_properties == null)
                throw new InvalidOperationException("Current object internal state is not valid");

            result = null;

            if (_properties.ContainsKey(binder.Name) )
            {
                string value = null;
                if (_properties.TryGetValue(binder.Name, out value))
                    result = value;
            }
            
            return true;
        }

        public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
        {
            if (indexes == null || indexes.Length <= 0)
                throw new IndexOutOfRangeException();

            if (indexes[0].GetType() != typeof(int) && indexes[0].GetType() != typeof(string))
                throw new ArgumentException("indexes");

            if ((value is string) == false)
                throw new ArgumentException("value is not stirng");

            string sanifiedInput = this.SanifyInput((string)value);

            if (_properties == null)
                throw new InvalidOperationException("Current object internal state is not valid");

            var index = indexes[0];
            string key = null;
            if (index is int)
            {
                int indexAsInt = (int)index;
                if (indexAsInt < 0 || indexAsInt >= _properties.Keys.Count)
                    throw new IndexOutOfRangeException();

                key = _properties.ElementAt(indexAsInt).Key;
            }
            else if (index is string)
            {
                key = (string)index;

                if (_properties.ContainsKey(key) == false && !string.IsNullOrEmpty(key))
                    _properties.TryAdd(key,null);
            }

            if (string.IsNullOrEmpty(key))
                throw new ArgumentException("indexes");

            if (_SYS_KEYS.Contains(key)) //prevent change of unmodificable parameter
                return true;

            _properties[key] = sanifiedInput;

            return true;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (string.IsNullOrEmpty(binder?.Name ?? string.Empty))
                throw new ArgumentException("binder.Name");

            if ((value is string) == false)
                throw new ArgumentException("value is not stirng");

            string sanifiedInput = this.SanifyInput((string)value);

            if (_properties == null)
                throw new InvalidOperationException("Current object internal state is not valid");

            if (_SYS_KEYS.Contains(binder.Name)) //prevent change of unmodificable parameter
                return true;

            if (_properties.ContainsKey(binder.Name) == false)
                _properties.TryAdd(binder.Name, sanifiedInput);
            else
                _properties[binder.Name] = sanifiedInput;

            return true;
        }

        public override string ToString()
        {
            string result = string.Empty;
            dynamic @this = this.ToDynamic;

            string sizeAsString = @this.__size.ToString();
            int size = (int)sizeAsString.FromInvariantString();

            StringBuilder builder = new StringBuilder();
            var keys = _properties?.Keys?.ToArray() ?? new string[0];
            foreach (string key in keys.OrderBy(k => k))
                builder.Append( key + _NAME_VALUE_SEPARATOR + _properties[key] + _ITEMS_SEPARATOR);

            if (builder.Length > size)
                throw new IndexOutOfRangeException($"Current data length is : {builder.Length}, but actual buffer expect maximum length of : {size}");

            if (builder.Length < size)
            {
                int paddingLength = size - builder.Length;
                builder.Append(this.GetRandomlyGenerateBase64String(paddingLength));
            }

            result = builder.ToString();
            return result;
        }

        /// <summary>
        /// Clone the current instance and get a new deep copy
        /// </summary>
        /// <returns></returns>
        public GenericToken Clone ()
        {
            return new GenericToken(_factoryRef, _properties);
        }

        /// <summary>
        /// Encrypt current token data to make it tamper proof and confidential
        /// </summary>
        /// <returns>A string representing the encrypted data inside the current token</returns>
        public string Encrypt()
        {
            return _factoryRef.Encrypt(this);
        }

        /// <summary>
        /// Check if the current token can be considered expired
        /// </summary>
        /// <param name="timeoutMs">Timeout in ms after which the curren token is considered expired (optional : if omitted token timeout is used)</param>
        /// <returns>A boolean value, true if the token is expired, false otherwhise</returns>
        public bool IsExpired (long timeoutMs = 0)
        {
            dynamic @this = this.ToDynamic;

            if (timeoutMs <= 0)
            {
                string tokenTimoutMsAsString = @this.__timeoutMs;
                long tokenTimeoutMs = tokenTimoutMsAsString.FromInvariantString();
                timeoutMs = tokenTimeoutMs > 0 ? tokenTimeoutMs : _DEF_EXPIRATION_MS;
            }

            string creationDateTimeTickAsString = @this.__creationTime;
            long creationDateTimeTicks = creationDateTimeTickAsString.FromInvariantString();

            DateTime creationDateTime = DateTime.MaxValue;
            if (creationDateTimeTicks > 0)
                creationDateTime = new DateTime(creationDateTimeTicks);

            return DateTime.Now.AddMilliseconds(-timeoutMs) > creationDateTime;
        }

        /// <summary>
        /// Check that the data stored are between the configured boundaries
        /// </summary>
        /// <returns>True if the data inserted are inside the boundaries, false otherwhise</returns>
        /// <remarks>For each field, field name and value will occupy space, but also separator characters.
        /// <para>{ Field1 = "value1" , Field2 = "value2" } will become : </para>
        /// <para>__␝standard␜Field1␝value1␜Field2␝value2␜xx..x</para></remarks>
        /// <para>As you can see there is a standard (not modifiable) field at the beginnging (few oher that are modifiable), your custom field, and optinal padding at the end. Everything separaterd by separator</para>
        public bool CheckBoundaries()
        {
            try
            {
                this.ToString();
                return true;
            }
            catch (Exception err)
            {
                return false;
            }
        }

        /// <summary>
        /// Generate a cryptographically secure string of chossen length
        /// </summary>
        /// <param name="length">The length of generated string</param>
        /// <returns>The cryptographically secoure string</returns>
        private string GetRandomlyGenerateBase64String(int length)
        {
            var random = new CryptoRandom();

            byte[] buffer = new byte[length];
            random.NextBytes(buffer);

            return Convert.ToBase64String(buffer).Substring(0,length);
        }

        /// <summary>
        /// Remove reserved character from input strings
        /// </summary>
        /// <param name="input">The input string to sanify</param>
        /// <returns>The sanified string</returns>
        private string SanifyInput (string input )
        {
            if (string.IsNullOrEmpty(input))
                input = string.Empty;

            return input.Replace(_ITEMS_SEPARATOR.ToString(), string.Empty)
                        .Replace(_NAME_VALUE_SEPARATOR.ToString(),string.Empty);
        }

    }
}

Extensions

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Token
{
    public static class Extensions
    {
        private static readonly CultureInfo _invariantCulture = CultureInfo.InvariantCulture;

        /// <summary>
        /// Convert an integer to its string representation with invariant culture
        /// </summary>
        public static string AsInvariantString ( this int @this)
        {
            return @this.ToString(_invariantCulture);
        }

        /// <summary>
        /// Convert an long to its string representation with invariant culture
        /// </summary>
        public static string AsInvariantString(this long @this)
        {
            return @this.ToString(_invariantCulture);
        }

        /// <summary>
        /// Convert a sting generated with AsInvariantString method back to its numeric value
        /// </summary>
        public static long FromInvariantString (this string @this)
        {
            long result = 0;
            if (long.TryParse(@this, out result) == false)
                result = 0;

            return result;
        }

    }
}
\$\endgroup\$
2
  • \$\begingroup\$ Please include the reset of the code, you can keep github repository as reference. \$\endgroup\$
    – iSR5
    Commented Aug 27, 2022 at 6:02
  • 1
    \$\begingroup\$ @iSR5 i added the code as requested, let me know if there is any trick to make it more readable (is a bit long) \$\endgroup\$
    – Skary
    Commented Aug 27, 2022 at 10:33

0