segunda-feira, 19 de maio de 2014

O problema das Cifras... E a dor de cabeça que levou uma noite a resolver!

Recentemente num projecto em que estive a trabalhar, tive de cifrar uma quantidade considerável de dados. A primeira coisa que me surgiu na mente foi usar hash, mas a necessidade de reverter a string cifrada para plain-text invalidou essa ideia.

Foi por essa altura que pensei "bem isto era porreiro encriptar em AES!", mas aí surgiu-me uma outra questão: Como o fazer ? Como gerar key-rings para cada registo ? Depois de indagar umas horas, acabei adoptando uma forma de encriptar registos e desencripar os mesmos de forma simples e quase instantânea, usando key-rings diferentes para cada registo.  

Problemas: 
  • Não repetir key-rings
  • Evitar paterns
  • Encriptar quantidades massivas de dados 
  • Fazê-lo de forma minimamente robusta 
A solução:

Umas pesquisas na web, não resultaram em grande coisa. Ou encontrava implementações que não funcionávam, ou tinham memory leaks, e nenhuma delas fazia tudo o que eu precisava.

Chegado a esta conclusão "volta-se ao quadro e desenha-se"! Depois de muitos rabiscos, lá começou a tomar forma aquilo que eu pretendia.

Uma classe que gera-se strings aleatórias de tamanho definido no construtor, e uma classe que suporta-se a encriptação, AES usando algoritmo Rijndael. Nesta faze decidi colocar a private key hardcoded na app, uma vez que o meu objectivo não se concentrava em proteger a app, mas proteger os dados, e o elo mais fraco seria o servidor de base de dados.


Neste caso a classe que encriptaria os dados é a seguinte:

Class: EncryptStringDino 
 


 using System;  
 using System.Text;  
 using System.Security.Cryptography;  
 using System.IO;  
   
 namespace EncryptStringDino  
 {  
   public static class StringCipher  
   {  
     private const string initVector = "EfK7yhF3HKywfvXp";  
   
     private const int keysize = 256;  
   
     //sumary  
     //metodo Encrypt  
     //exemplo: string cifrado = Encrypt(texto_a_cifrar, key)  
     //key é uma contra-senha que tanto pode ser uma constante como um valor de uma veriável  
     //devolve uma string correspondente ao texto a cifrar, cifrado recorrendo ao standard AES metodologia Rijndael  
     public static string Encrypt(string plainText, string passPhrase)  
     {  
       byte[] initVectorBytes = Encoding.UTF8.GetBytes(initVector);  
       byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);  
       PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);  
       byte[] keyBytes = password.GetBytes(keysize / 8);  
       RijndaelManaged symmetricKey = new RijndaelManaged();  
       symmetricKey.Mode = CipherMode.CBC;  
       ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes);  
       MemoryStream memoryStream = new MemoryStream();  
       CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);  
       cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);  
       cryptoStream.FlushFinalBlock();  
       byte[] cipherTextBytes = memoryStream.ToArray();  
       memoryStream.Close();  
       cryptoStream.Close();  
       return Convert.ToBase64String(cipherTextBytes);  
     }  
   
     //sumary  
     //metodo Dncrypt  
     //exemplo: string cifrado = Encrypt(texto_cifrado, key)  
     //key é uma contra-senha que tanto pode ser uma constante como um valor de uma veriável  
     //devolve uma string correspondente ao texto a cifrar, cifrado recorrendo ao standard AES metodologia Rijndael  
     public static string Decrypt(string cipherText, string passPhrase)  
     {  
       byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);  
       byte[] cipherTextBytes = Convert.FromBase64String(cipherText);  
       PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);  
       byte[] keyBytes = password.GetBytes(keysize / 8);  
       RijndaelManaged symmetricKey = new RijndaelManaged();  
       symmetricKey.Mode = CipherMode.CBC;  
       ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes, initVectorBytes);  
       MemoryStream memoryStream = new MemoryStream(cipherTextBytes);  
       CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);  
       byte[] plainTextBytes = new byte[cipherTextBytes.Length];  
       int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);  
       memoryStream.Close();  
       cryptoStream.Close();  
       return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);  
     }  
   }  
 }  


 Class: RamdomStringGeneratorDino , que vai gerar as strings aleatórias que vão fazer parte do processo de encriptação.

Este código foi alterado diversas vezes porque causava constantemente erros e problemas com a CLR, ao ponto de ter de ser alterado de novo, quando começou a gerar exceptions quando pedido que gera-se um volume de cerca de 2000 strings. 

Class: RamdomStringGeneratorDino
  
 using System;  
 using System.Collections.Generic;  
 using System.Linq;  
 using System.Security.Cryptography;  
   
 namespace RamdomStringGeneratorDino  
 {  
   /// <summary>  
   /// Classe geradora de strings aleatórias de acordo com as opções abaixo listadas  
   /// 1) 4 caracteres (maiusculo, minusculo, numerico e caracteres especiais)  
   /// 2) numero variável de caracteres em uso  
   /// 3) numero minimo de caracteres de cada tipo a serem usados na string  
   /// 4) Geração orientada a patterns  
   /// 5) geração de strings unicas  
   /// 6) usar cada caracter apenas uma vez  
   /// feito para gerar "keyt" para senhas Rjindael  
   /// </summary>  
   public class RandomStringGenerator  
   {  
     public RandomStringGenerator(bool UseUpperCaseCharacters = true,  
                    bool UseLowerCaseCharacters = true,  
                    bool UseNumericCharacters = true,  
                    bool UseSpecialCharacters = true)  
     {  
       m_UseUpperCaseCharacters = UseUpperCaseCharacters;  
       m_UseLowerCaseCharacters = UseLowerCaseCharacters;  
       m_UseNumericCharacters = UseNumericCharacters;  
       m_UseSpecialCharacters = UseSpecialCharacters;  
       CurrentGeneralCharacters = new char[0]; // evita excepções de null  
       UpperCaseCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();  
       LowerCaseCharacters = "abcdefghijklmnopqrstuvwxyz".ToCharArray();  
       NumericCharacters = "0123456789".ToCharArray();  
       SpecialCharacters = ",.;:?!/@#$%^&()=+*-_{}[]<>|~".ToCharArray();  
       MinUpperCaseCharacters = MinLowerCaseCharacters = MinNumericCharacters = MinSpecialCharacters = 0;  
       RepeatCharacters = true;  
       PatternDriven = false;  
       Pattern = "";  
       Random = new RNGCryptoServiceProvider();  
       ExistingStrings = new List<string>();  
     }  
   
     #region character sets managers  
     /// <summary>  
     /// True se precisar-mos de um numero de caracteres fixo  
     /// </summary>  
     public bool UseUpperCaseCharacters  
     {  
       get  
       {  
         return m_UseUpperCaseCharacters;  
       }  
       set  
       {  
         if (CurrentUpperCaseCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentUpperCaseCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentUpperCaseCharacters).ToArray();  
         m_UseUpperCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obter a definição de caracteres maiusculos.  
     /// </summary>  
     public char[] UpperCaseCharacters  
     {  
       get  
       {  
         return CurrentUpperCaseCharacters;  
       }  
       set  
       {  
         if (UseUpperCaseCharacters)  
         {  
           if (CurrentUpperCaseCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentUpperCaseCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentUpperCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// True se é obritatório o uso de minusculas  
     /// </summary>  
     public bool UseLowerCaseCharacters  
     {  
       get  
       {  
         return m_UseLowerCaseCharacters;  
       }  
       set  
       {  
         if (CurrentLowerCaseCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentLowerCaseCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentLowerCaseCharacters).ToArray();  
         m_UseLowerCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obtem a definição de uso de caracteres minusculos  
     /// </summary>  
     public char[] LowerCaseCharacters  
     {  
       get  
       {  
         return CurrentLowerCaseCharacters;  
       }  
       set  
       {  
         if (UseLowerCaseCharacters)  
         {  
           if (CurrentLowerCaseCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentLowerCaseCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentLowerCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// True se é necessário o uso de caracteres numéricos  
     /// </summary>  
     public bool UseNumericCharacters  
     {  
       get  
       {  
         return m_UseNumericCharacters;  
       }  
       set  
       {  
         if (CurrentNumericCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentNumericCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentNumericCharacters).ToArray();  
         m_UseNumericCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obtem a definição de uso de caracteres numéricos  
     /// </summary>  
     public char[] NumericCharacters  
     {  
       get  
       {  
         return CurrentNumericCharacters;  
       }  
       set  
       {  
         if (UseNumericCharacters)  
         {  
           if (CurrentNumericCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentNumericCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentNumericCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// True se é para usar caracteres especiais  
     /// </summary>  
     public bool UseSpecialCharacters  
     {  
       get  
       {  
         return m_UseSpecialCharacters;  
       }  
       set  
       {  
         if (CurrentSpecialCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentSpecialCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentSpecialCharacters).ToArray();  
         m_UseSpecialCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obtem a definição de uso de caracteres especiais  
     /// </summary>  
     public char[] SpecialCharacters  
     {  
       get  
       {  
         return CurrentSpecialCharacters;  
       }  
       set  
       {  
         if (UseSpecialCharacters)  
         {  
           if (CurrentSpecialCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentSpecialCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentSpecialCharacters = value;  
       }  
     }  
     #endregion  
   
     #region character limits  
     /// <summary>  
     /// Define ou obtem o numero minumo de caracteres maiusculos a serem usados.  
     /// </summary>  
     public int MinUpperCaseCharacters  
     {  
       get { return m_MinUpperCaseCharacters; }  
       set { m_MinUpperCaseCharacters = value; }  
     }  
   
     /// <summary>  
     /// Define ou obtem o numero minimo de caracteres minusculos a serem usados.  
     /// </summary>  
     public int MinLowerCaseCharacters  
     {  
       get { return m_MinLowerCaseCharacters; }  
       set { m_MinLowerCaseCharacters = value; }  
     }  
   
     /// <summary>  
     /// define ou obtem o numero minimo de caracteres numéricos a sere utilizados.  
     /// </summary>  
     public int MinNumericCharacters  
     {  
       get { return m_MinNumericCharacters; }  
       set { m_MinNumericCharacters = value; }  
     }  
   
     /// <summary>  
     /// define ou obtem o numero minimo de caracteres especiais a serem utilizados.  
     /// </summary>  
     public int MinSpecialCharacters  
     {  
       get { return m_MinSpecialCharacters; }  
       set { m_MinSpecialCharacters = value; }  
     }  
     #endregion  
   
     #region pattern  
     private string m_pattern;  
   
     /// <summary>  
     /// Define o padrão a ser seguido para gerar uma string.   
     /// Este valor é ignorado se for igual string vazia.   
     /// Os padrões são:   
     /// L - para letra maiúscula   
     /// L - para letra minúscula   
     /// N - de número   
     /// S - para caractere especial   
     /// * - Para qualquer caractere  
     /// </summary>  
     private string Pattern  
     {  
       get  
       {  
         return m_pattern;  
       }  
       set  
       {  
         if (!value.Equals(String.Empty))  
           PatternDriven = true;  
         else  
           PatternDriven = false;  
         m_pattern = value;  
       }  
     }  
     #endregion  
   
     #region generators  
     /// <summary>  
     /// Gerar uma string que segue o padrão.   
     /// Caracteres possíveis são:   
     /// L - para letra maiúscula   
     /// L - para letra minúscula   
     /// N - de número   
     /// S - para caractere especial   
     /// * - Para qualquer caracter  
     /// </summary>  
     /// <param name="Pattern">o pattern na ser seguido enquanto gera as strings</param>  
     /// <returns>um padrão aleatório que é retornado após a geração</returns>  
     public string Generate(string Pattern)  
     {  
       this.Pattern = Pattern;  
       string res = GenerateString(Pattern.Length);  
       this.Pattern = "";  
       return res;  
     }  
   
     /// <summary>  
     /// gera uma string de comprimento variável compreendido entre MinLength e MaxLength. Os caracteres   
     /// devem ser definidos antes de ser chamada esta função  
     /// </summary>  
     /// <param name="MinLength">cumprimento minimo da string string</param>  
     /// <param name="MaxLength">Cumprimento maximo da string</param>  
     /// <returns>uma string aleatória de um tamanho compreendido entre o minimo e o maximo</returns>  
     public string Generate(int MinLength, int MaxLength)  
     {  
       if (MaxLength < MinLength)  
         throw new ArgumentException("Maximal length should be grater than minumal");  
       int length = MinLength + (GetRandomInt() % (MaxLength - MinLength));  
       return GenerateString(length);  
     }  
   
     /// <summary>  
     /// Gera uma string de comprimento fixo  
     /// os conjuntos de caracteres utilizaveis devem ser definidos antes de chamar esta função  
     /// </summary>  
     /// <param name="FixedLength">cumprimento da string</param>  
     /// <returns>uma string aleatória do comprimento desejado</returns>  
     public string Generate(int FixedLength)  
     {  
       return GenerateString(FixedLength);  
     }  
   
     /// <summary>  
     /// Metodo de geração principal que escolhe o metodo de geração adequado.  
     /// procura situações excepcionais também.  
     /// </summary>  
     private string GenerateString(int length)  
     {  
       if (length == 0)  
         throw new ArgumentException("Não se pode gerar uma string com zero caracteres");  
       if (!UseUpperCaseCharacters && !UseLowerCaseCharacters && !UseNumericCharacters && !UseSpecialCharacters)  
         throw new ArgumentException("Tem de se usar pelo menos um conjunto de caracteres! É que é burro alvin! :D");  
       if (!RepeatCharacters && (CurrentGeneralCharacters.Length < length))  
         throw new ArgumentException("não existem caracteres suficientes para gerar a string sem repetir caracteres");  
       string result = ""; // Esta string contem o resultado  
       if (PatternDriven)  
       {  
         // usando a pattern para gerar algo  
         result = PatternDrivenAlgo(Pattern);  
       }  
       else if (MinUpperCaseCharacters == 0 && MinLowerCaseCharacters == 0 &&  
            MinNumericCharacters == 0 && MinSpecialCharacters == 0)  
       {  
         // usando o algoritmo mais simples, neste caso  
         result = SimpleGenerateAlgo(length);  
       }  
       else  
       {  
         // atenção ao limite  
         result = GenerateAlgoWithLimits(length);  
       }  
       // suporte para strings unicas  
       // recursão, a possibilidade de stack overflow é grande para strings maiores que 3 chars.  
       try  
       {  
         if (UniqueStrings && ExistingStrings.Contains(result))  
           return GenerateString(length);  
         AddExistingString(result); // guarda histórico  
       }  
       catch { throw; }// intercepta o overflow e manda-o de volta (pro raio que o parta)  
       return result;  
     }  
   
     /// <summary>  
     /// gera uma string aleatória baseada na pattern  
     /// </summary>  
     private string PatternDrivenAlgo(string Pattern)  
     {  
       string result = "";  
       List<char> Characters = new List<char>();  
       foreach (char character in Pattern.ToCharArray())  
       {  
         char newChar = ' ';  
         switch (character)  
         {  
           case 'L':  
             {  
               newChar = GetRandomCharFromArray(CurrentUpperCaseCharacters, Characters);  
               break;  
             }  
           case 'l':  
             {  
               newChar = GetRandomCharFromArray(CurrentLowerCaseCharacters, Characters);  
               break;  
             }  
           case 'n':  
             {  
               newChar = GetRandomCharFromArray(CurrentNumericCharacters, Characters);  
               break;  
             }  
           case 's':  
             {  
               newChar = GetRandomCharFromArray(CurrentSpecialCharacters, Characters);  
               break;  
             }  
           case '*':  
             {  
               newChar = GetRandomCharFromArray(CurrentGeneralCharacters, Characters);  
               break;  
             }  
           default:  
             {  
               throw new Exception("O caracter '" + character + "' não é suportado");  
             }  
         }  
         Characters.Add(newChar);  
         result += newChar;  
       }  
       return result;  
     }  
   
     /// <summary>  
     ///   
     ///   
     /// </summary>  
     private string SimpleGenerateAlgo(int length)  
     {  
       string result = "";  
   
       for (int i = 0; i < length; i++)  
       {  
         char newChar = CurrentGeneralCharacters[GetRandomInt() % CurrentGeneralCharacters.Length];  
         if (!RepeatCharacters && result.Contains(newChar))  
         {  
           do  
           {  
             newChar = CurrentGeneralCharacters[GetRandomInt() % CurrentGeneralCharacters.Length];  
           } while (result.Contains(newChar));  
         }  
         result += newChar;  
       }  
       return result;  
     }  
   
     /// <summary>  
     ///   
     /// </summary>  
     private string GenerateAlgoWithLimits(int length)  
     {  
   
       if (MinUpperCaseCharacters + MinLowerCaseCharacters +  
         MinNumericCharacters + MinSpecialCharacters > length)  
       {  
         throw new ArgumentException("Sum of MinUpperCaseCharacters, MinLowerCaseCharacters," +  
           " MinNumericCharacters and MinSpecialCharacters is greater than length");  
       }  
       if (!RepeatCharacters && (MinUpperCaseCharacters > CurrentUpperCaseCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinUpperCaseCharacters");  
       if (!RepeatCharacters && (MinLowerCaseCharacters > CurrentLowerCaseCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinLowerCaseCharacters");  
       if (!RepeatCharacters && (MinNumericCharacters > CurrentNumericCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinNumericCharacters");  
       if (!RepeatCharacters && (MinSpecialCharacters > CurrentSpecialCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinSpecialCharacters");  
       int AllowedNumberOfGeneralChatacters = length - MinUpperCaseCharacters - MinLowerCaseCharacters  
         - MinNumericCharacters - MinSpecialCharacters;  
   
       string result = "";  
   
       List<char> Characters = new List<char>();  
   
   
       for (int i = 0; i < MinUpperCaseCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(UpperCaseCharacters, Characters));  
       for (int i = 0; i < MinLowerCaseCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(LowerCaseCharacters, Characters));  
       for (int i = 0; i < MinNumericCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(NumericCharacters, Characters));  
       for (int i = 0; i < MinSpecialCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(SpecialCharacters, Characters));  
       for (int i = 0; i < AllowedNumberOfGeneralChatacters; i++)  
         Characters.Add(GetRandomCharFromArray(CurrentGeneralCharacters, Characters));  
   
   
       for (int i = 0; i < length; i++)  
       {  
         int position = GetRandomInt() % Characters.Count;  
         char CurrentChar = Characters[position];  
         Characters.RemoveAt(position);  
         result += CurrentChar;  
       }  
       return result;  
     }  
   
     #endregion  
   
   
     public bool RepeatCharacters;  
   
   
     public bool UniqueStrings;  
   
   
     public void AddExistingString(string s)  
     {  
       ExistingStrings.Add(s);  
     }  
   
     #region misc tools  
   
     private int GetRandomInt()  
     {  
       byte[] buffer = new byte[2]; // 16 bit = 2^16 = 65576   
       Random.GetNonZeroBytes(buffer);  
       int index = BitConverter.ToInt16(buffer, 0);  
       if (index < 0)  
         index = -index; //handle de numeros negativos  
       return index;  
     }  
   
     private char GetRandomCharFromArray(char[] array, List<char> existentItems)  
     {  
       char Character = ' ';  
       do  
       {  
         Character = array[GetRandomInt() % array.Length];  
       } while (!RepeatCharacters && existentItems.Contains(Character));  
       return Character;  
     }  
     #endregion  
   
     #region internal state  
     private bool m_UseUpperCaseCharacters, m_UseLowerCaseCharacters, m_UseNumericCharacters, m_UseSpecialCharacters;  
     private int m_MinUpperCaseCharacters, m_MinLowerCaseCharacters, m_MinNumericCharacters, m_MinSpecialCharacters;  
     private bool PatternDriven;  
     private char[] CurrentUpperCaseCharacters;  
     private char[] CurrentLowerCaseCharacters;  
     private char[] CurrentNumericCharacters;  
     private char[] CurrentSpecialCharacters;  
     private char[] CurrentGeneralCharacters;  
     private RNGCryptoServiceProvider Random;  
     private List<string> ExistingStrings;  
     #endregion  
   }  
 }  
   

E por fim o código que utilizei para cifrar os dados, depois de os ter numa datagridview em windows forms:

   
  foreach (DataGridViewCell cell in row.Cells)  
           {  
             deviceid = row.Cells[0].Value.ToString();  
             plainText = row.Cells[3].Value.ToString(); //lê o código da celula 4 da grid  passPhrase = RSG.Generate(7); //gera a contrasenha  
             passcritped = EncryptStringDino.StringCipher.Encrypt(plainText, passPhrase); //encripta e armazena o valor na variável na passcripted  
             SqlCommand commands2 = new SqlCommand("UPDATE tabela SET codigo = '" + passcritped + "' , key = '" + passPhrase + "' WHERE campo1 = '" + arg1 + "' ;", conex); //conex é a conection string à base de dados 
             commands2.ExecuteNonQuery();  
               
             richTextBox1.AppendText(Environment.NewLine + arg1 + ";");  
           }  
 
E pronto, foi esta a solução que dei à questão.

Provávelmente existem soluções melhores que esta. De futuro pensarei noutras. Também estou a ponderar portar ambas as classes para .net 4.5 usando async e await para evitar alguma lentidão quando o volume de registos é grande.

E pronto, fica aqui um pedaço de código que pode dar jeito a alguém e foi implementado numa noite de insónias.

"Enquanto houver paixão pela programação e café... Haverá código!"

Nenhum comentário:

Postar um comentário

Observação: somente um membro deste blog pode postar um comentário.