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
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!"