LGPD + EF CORE + ValueConverter

30 minuto(s) de leitura - July 02, 2023

01

Olá tudo bem?!

Mais 1 artigo??? Desculpa estou de férias!!!

Bom, primeiramente o objetivo não é falar sobre LGPD, como conceitos, onde se aplica, como funciona,... nada disso, apenas mostrar que podemos proteger nossos dados de forma segura e simples usando Entity Framework Core.

Mas pera aí, você não vai falar nada de LGPD?... tá bom, LGPD é um acrônimo para (Lei Geral de Proteção de Dados Pessoais) que basicamente o Brasil adotou depois que alguns paises da Europa começaram exigir que o GDPR fosse implementado, para que os dados dos cidadões e sua privacidade pudesse estar segura.
Basicamente de forma muito resumida é isso... antes de criticas observe que falei "BASICAMENTE".
Você pode acessar os links abaixo para obter mais informações:
GDPR
LGPD

Cenário

Imagine que você está usando o EF Core e precisa armazenar informações de algumas propriedas específicas criptografadas em sua base de dados, para garantir a integridade da informação e que os dados sejam exibidos apenas pelo sistema, ou pelo dono da informação, que para nosso exemplo será nossa própria aplicação.

Fulando: Rafael com todo respeito isso é fácil!
Rafael: Tudo bem, só acredito que posso 
        tornar ainda mais fácil.

Bom vamos começar a montar nosso sistema de cadastro de clientes, onde teremos uma classe Cliente com a seguinte estrutura.
public class Cliente
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public string Telefone { get; set; }
    public string Endereco { get; set; }
    public string CPF { get; set; }
}
Agora vamos criar nossa classe de contexto para acessar o banco de dados, basicamente essa é a estrutura da classe:
 public class DatabaseContext : DbContext
{
    public DbSet<Cliente> Clientes { get; set; }

    protected override void OnConfiguring(
        DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Server=(localdb)\msSqlLocalDB;Integrated Security=True;Database=EFCoreValueConvertion;MultipleActiveResultSets=true;");
}
Vamos inserir um cliente em nossa base de dados e fazer uma consulta.
public class Program
{
    static void Main(string[] args)
    {
        using var db = new DatabaseContext();
        db.Database.EnsureCreated();

        db.Clientes.Add(new Cliente
        {
            Nome = "Rafael Almeida",
            Endereco = "Aqui mesmo",
            Telefone = "7998829XXXX",
            CPF = "123456"
        });

        db.SaveChanges();

        var cliente = db
            .Clientes
            .AsNoTracking()
            .FirstOrDefault(p => p.CPF == "123456");
    }
}

Comandos gerados

Os comandos produzidos pelo EF Core foram esses:
Comando Inserir

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Clientes] ([CPF], [Endereco], [Nome], [Telefone])
VALUES (@p0, @p1, @p2, @p3);
SELECT [Id]
FROM [Clientes]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(4000),@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 nvarchar(4000)',
@p0=N'123456',@p1=N'Aqui mesmo',@p2=N'Rafael Almeida',@p3=N'7998829XXXX'

Comando Consultar

SELECT TOP(1) [c].[Id], [c].[CPF], [c].[Endereco], [c].[Nome], [c].[Telefone]
FROM [Clientes] AS [c]
WHERE [c].[CPF] = N'123456'

Protegendo dados explícitamente

Até aqui tudo normal, nada de novo, então vamos voltar ao assunto de proteger os dados?!
Mas eu gostaria que Telefone e CPF, seja armazenado de forma criptografada, você poderia apenas criar uma função para criptografar os dados no momento que for persistir, e quando consultar descriptografar os dados.
Perfeito, então vejo você fazendo algo assim:

public class Program
{
    static void Main(string[] args)
    {
        using var db = new DatabaseContext();
        db.Database.EnsureCreated();
        db.Clientes.Add(new Cliente
        {
            Nome = "Rafael Almeida",
            Endereco = "Aqui mesmo",
            Telefone = LockView("7998829XXXX"), //Criptografando
            CPF = LockView("123456") // Criptografando
        });

        db.SaveChanges();

        var cliente = db
            .Clientes
            .AsNoTracking()
            .FirstOrDefault(p => p.CPF == LockView("123456"));

        var cpf = UnLockView(cliente.CPF);
    }

    static string LockView(string texto)
    {
        using var hashProvider = new MD5CryptoServiceProvider();
        var encriptar = new TripleDESCryptoServiceProvider
        {
            Mode = CipherMode.ECB,
            Key = hashProvider.ComputeHash(_chave),
            Padding = PaddingMode.PKCS7
        };

        using var transforme = encriptar.CreateEncryptor();
        var dados = Encoding.UTF8.GetBytes(texto);
        return Convert.ToBase64String(transforme.TransformFinalBlock(dados, 0, dados.Length));
    }
}

Delegando responsabilidade

Funciona perfeitamente, não é a melhor maneira de fazer, então podemos melhorar isso e delegar a responsabilidade para o EF Core, vamos criar um atributo e extrair funcionalidades que o EF Core nos proporciona, nesse caso primeiramente vamos criar nosso atributo SensitiveData.

[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class SensitiveDataAttribute : Attribute
{
}

Agora vamos adicionar o atributo em todas propriedades que queremos que o EF Core fique responsável pelo trabalho pesado, de armazenar e ler os dados sensíveis.

public class Cliente
{
    public int Id { get; set; }
    public string Nome { get; set; }
    [SensitiveData]
    public string Telefone { get; set; }
    public string Endereco { get; set; }
    [SensitiveData]
    public string CPF { get; set; }
}
Agora vamos criar uma classe customizada para ficar responsável pela conversão dos valores que serão persistidos e lidos de nossa base de dados, a partir do EF Core 2.1 temos uma nova funcionalidade que basicamente nos fornece uma forma na qual podemos manipular o dado antes de ser persistido e quando for lido também, essa funcionalidade se chama ValueConverter, em breve irei fazer um artigo falando mais sobre ela, por enquanto vamos focar aqui, e nossa classe customizada ficou da seguinte forma com 2 metódos, para critografar e descriptografar os dados, o que fiz foi criar um classe que herda da classe ValueConverter alguns comportamentos, onde em nosso construtor eu passo a expressão que eu quero utilizar para persitir e ler dados.
public class DataProtectionConverter : ValueConverter<string, string>
{
    private static byte[] _chave = Encoding.UTF8.GetBytes("#lgpd+ef");

    public DataProtectionConverter()
        : base(_convertTo, _convertFrom, default)
    {
    }

    static Expression<Func<string, string>> _convertTo = x => LockView(x);
    static Expression<Func<string, string>> _convertFrom = x => UnLockView(x);

    static string LockView(string texto)
    {
        using var hashProvider = new MD5CryptoServiceProvider();
        var encriptar = new TripleDESCryptoServiceProvider
        {
            Mode = CipherMode.ECB,
            Key = hashProvider.ComputeHash(_chave),
            Padding = PaddingMode.PKCS7
        };

        using var transforme = encriptar.CreateEncryptor();
        var dados = Encoding.UTF8.GetBytes(texto);
        return Convert.ToBase64String(transforme.TransformFinalBlock(dados, 0, dados.Length));
    }

    static string UnLockView(string texto)
    {
        using var hashProvider = new MD5CryptoServiceProvider();
        var descriptografar = new TripleDESCryptoServiceProvider
        {
            Mode = CipherMode.ECB,
            Key = hashProvider.ComputeHash(_chave),
            Padding = PaddingMode.PKCS7
        };

        using var transforme = descriptografar.CreateDecryptor();
        var dados = Convert.FromBase64String(texto.Replace(" ", "+"));
        return Encoding.UTF8.GetString(transforme.TransformFinalBlock(dados, 0, dados.Length));
    }
}
Classe criada, agora vamos ajustar nosso DatabaseContext da seguinte forma:
public class DatabaseContext : DbContext
{
    public DbSet<Cliente> Clientes { get; set; }

    protected override void OnConfiguring(
        DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\msSqlLocalDB;Integrated Security=True; Database=EFCoreValueConvertion; MultipleActiveResultSets=true;"
            );

    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Selecionar todas propriedades que tem a anotação SensitiveData
        // e aplicar o conversor de valores para o que criamos.
        foreach (var entity in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in entity.GetProperties())
            {
                var attributes = property
                    .PropertyInfo
                    .GetCustomAttributes(typeof(SensitiveDataAttribute), false);

                if (attributes.Length > 0)
                {
                    property.SetValueConverter(new DataProtectionConverter());
                }
            }
        }
    }
}

Código final

Agora como você pode ver não iremos precisar mais ficar criptografando explicitamente as informações, nosso exemplo completo ficou assim:

public class Program
{
    static void Main(string[] args)
    {

        using var db = new DatabaseContext();
        db.Database.EnsureCreated();
        db.Clientes.Add(new Cliente
        {
            Nome = "Rafael Almeida",
            Endereco = "Aqui mesmo",
            Telefone = "7998829XXXX",
            CPF = "123456"
        });

        db.SaveChanges();

        var cliente = db.Clientes.AsNoTracking().FirstOrDefault(p => p.CPF == "123456");
    }
}

// SensitiveDataAttribute
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class SensitiveDataAttribute : Attribute
{
}

// Cliente
public class Cliente
{
    public int Id { get; set; }
    public string Nome { get; set; }
    [SensitiveData]                     // SensitiveData
    public string Telefone { get; set; }
    public string Endereco { get; set; }
    [SensitiveData]                     // SensitiveData
    public string CPF { get; set; }
}


// DatabaseContext
public class DatabaseContext : DbContext
{
    public DbSet<Cliente> Clientes { get; set; }

    protected override void OnConfiguring(
        DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\msSqlLocalDB;Integrated Security=True; Database=EFCoreValueConvertion; MultipleActiveResultSets=true;"
            );

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entity in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in entity.GetProperties())
            {
                var attributes = property
                    .PropertyInfo
                    .GetCustomAttributes(typeof(SensitiveDataAttribute), false);

                if (attributes.Length > 0)
                {
                    property.SetValueConverter(new DataProtectionConverter());
                }
            }
        }
    }
}

// DataProtectionConverter
public class DataProtectionConverter : ValueConverter<string, string>
{
    private static byte[] _chave = Encoding.UTF8.GetBytes("#lgpd+ef");

    public DataProtectionConverter()
        : base(_convertTo, _convertFrom, default)
    {
    }

    private static Expression<Func<string, string>> _convertTo = x => LockView(x);
    private static Expression<Func<string, string>> _convertFrom = x => UnLockView(x);

    static string LockView(string texto)
    {
        using var hashProvider = new MD5CryptoServiceProvider();
        var encriptar = new TripleDESCryptoServiceProvider
        {
            Mode = CipherMode.ECB,
            Key = hashProvider.ComputeHash(_chave),
            Padding = PaddingMode.PKCS7
        };

        using var transforme = encriptar.CreateEncryptor();
        var dados = Encoding.UTF8.GetBytes(texto);
        return Convert.ToBase64String(transforme.TransformFinalBlock(dados, 0, dados.Length));
    }

    static string UnLockView(string texto)
    {
        using var hashProvider = new MD5CryptoServiceProvider();
        var descriptografar = new TripleDESCryptoServiceProvider
        {
            Mode = CipherMode.ECB,
            Key = hashProvider.ComputeHash(_chave),
            Padding = PaddingMode.PKCS7
        };

        using var transforme = descriptografar.CreateDecryptor();
        var dados = Convert.FromBase64String(texto.Replace(" ", "+"));
        return Encoding.UTF8.GetString(transforme.TransformFinalBlock(dados, 0, dados.Length));
    }
}

Output SQL

Os comandos produzidos ficaram assim:
Comando Inserir

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Clientes] ([CPF], [Endereco], [Nome], [Telefone])
VALUES (@p0, @p1, @p2, @p3);
SELECT [Id]
FROM [Clientes]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(4000),@p1 nvarchar(4000),@p2 nvarchar(4000),@p3 nvarchar(4000)',
@p0=N'kOI/e7VQZhs=', -- Criptografado
@p1=N'Aqui mesmo',@p2=N'Rafael Almeida',
@p3=N'T2wQKyR8w28fKOgBXp0ytg=='    -- Criptografado

Comando Consultar

SELECT TOP(1) [c].[Id], [c].[CPF], [c].[Endereco], [c].[Nome], [c].[Telefone]
FROM [Clientes] AS [c]
WHERE [c].[CPF] = N'kOI/e7VQZhs='

01

Observações

Alguns banco de dados já fornecem criptografia de ponta-a-ponta, um banco de dados é apenas uma das ferramentas que podemos usar para que possamos estar em conformidade com LGPD/GDPR, uma dica é fique de olho na profissão de DPO (Data Protection Officer), será uma profissão que terá muitas vagas para os próximos anos, muitas empresas vão precisar desse profissional.

Twitter

Fico por aqui! 😄
Me siga no twitter: @ralmsdeveloper


Categorias:

Atualizado em:

Deixe um comentário