Aujourd’hui, je souhaite vous faire un feedback sur la mise en place d’une solution de chiffrement manuelle des données avec Entity Framework et une base de données.

Un peu de contexte

Le système sur lequel j’interviens nécessite la mise en place d’un mécanisme de chiffrement des données. Il permet de chiffrer des colonnes spécifiques (celles contenant des données sensibles) tout en conservant la clé côté client et non côté serveur (pour éviter, par exemple, à un DBA d’accéder aux données).

Avec SQL Server, j’aurais tout de suite pensé à la fonctionnalité Always Encrypted. Mais le SGBD utilisé étant MySQL, il n’y a, à ma connaissance, par d’alternative built-in à cette fonctionnalité.

Concernant la partie chiffrement à proprement parler, mon très cher collègue Emilien GUILMINEAU en a déjà parlé dans cet article, que je vous invite à aller lire, il est excellent ;).

Ce qui nous intéresse aujourd’hui est comment intégrer cela avec Entity Framework de la manière la plus smooth possible. Et c’est ce que nous allons voir tout de suite !

1ère étape : identifier les colonnes

La 1ère étape consiste à trouver un moyen d’identifier, dans notre modèle C#, les colonnes qui sont concernées par le chiffrement. Pour cela, un excellent moyen est d’utiliser les attributs.

C’est quoi un attribut ?

Il s’agit d’un concept du langage C# qui permet d’associer des métadonnées à du code (classe, propriété, méthode…). Une fois associé, l’attribut peut-être récupéré à l’exécution via la réflexion.

Vous en avez probablement déjà utilisé, il en existe plusieurs qui sont très connus : Obsolete, Autorize, Serializable

https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/attributes

Pour créer un attribut personnalisé, c’est assez simple : il suffit d’écrire une classe héritant de Attribute.

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

Décortiquons un peu ce code :

  • Commençons par le nom, qui suit la convention [Nom de l'attribut][Suffixe *Attribute*]
  • On peut bien sûr avoir des paramètres ou des propriétés, qui pourront être initialisés via un constructeur (ce n’est pas le cas ici)
  • A noter que notre classe possède elle-même un attribut, AttributeUsage, qui permet de définir :
    • la validité de l’attribut (ici, uniquement les propriétés)
    • Inherited, qui permet d’indiquer le comportant de l’attribut vis-à-vis de l’héritage (autrement dit, est-ce que l’attribut d’un parent est appliqué aux classes enfants)
    • AllowMultiple, qui permet d’indiquer si l’on peut utiliser l’attribut plusieurs fois sur le même élément

Et voici comment on utilise cet attribut au niveau de notre modèle :

public class MyEntity
{
    public Guid Id { get; set; }

    public string PublicData { get; set; }

    [Encrypted]
    public string PrivateData { get; set; }
}

Vous noterez bien qu’à l’utilisation, on omet le suffixe Attribute.

2ème étape : prise en compte dans Entity Framework

Une fois que l’on est capable d’identifier les colonnes concernées par le chiffrement, il faut qu’Entity Framework prenne en compte cet attribut et applique l’opération adaptée (chiffrement ou déchiffrement), et ce, de la manière la plus transparente possible.

Pour cela, on va utiliser 2 concepts d’Entity Framework :

  • la méthode OnModelCreating
  • les value converters

La méthode OnModelCreating permet de venir configurer le modèle, sans modifier les classes des entités. On va donc l’utiliser pour parcourir les propriétés de notre modèle à la recherche de notre attribut Encrypted (via la réflexion).

public class MyDbContext : DbContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // ... Describe entities ...

        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                var attributes = property.GetCustomAttributes(typeof(EncryptedAttribute), false);
                if (attributes.Any())
                {
                    // TODO: Do magic !
                }
            }
        }
    }
}

Passons désormais à la ligne TODO: Do magic !. Si nous sommes arrivés ici, c’est que la propriété courante possède l’attribut Encrypted. L’étape suivante est donc d’appliquer soit l’opération de chiffrement, soit celle de déchiffrement.

Pour cela, nous allons utiliser les value converters ! Il s’agit d’une classe permettant de convertir les valeurs des propriétés lors de la lecture ou de l’écriture dans une base de données. C’est donc parfait pour notre cas, car cela va nous permettre d’appliquer la bonne opération en fonction du sens (lecture ou écriture).

Dans Entity Framework, il existe de nombreux convertisseurs intégrés : BoolToStringConverter, DateTimeToStringConverter, EnumToNumberConverter, StringToGuidConverter

Ils peuvent être utilisés directement lors de la configuration du modèle via la méthode HasConversion<T>() ou HasConversion(converter).

https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions?tabs=data-annotations

Dans notre cas, il n’existe pas de convertisseur intégré qui fait ce l’on souhaite. On va donc l’écrire !

public class EncryptedConverter : ValueConverter<string, string>
{
    public EncryptedConverter(ConverterMappingHints mappingHints = default)
        : base(EncryptExpr, DecryptExpr, mappingHints)
    { }

    static Expression<Func<string, string>> DecryptExpr = x => CryptoService.Decrypt(x);
	static Expression<Func<string, string>> EncryptExpr = x => CryptoService.Encrypt(x);
}

Décortiquons un peu ce code :

  • ValueConverter<string, string> indique que notre convertisseur prend en entrée une chaine de caractères et produit en sortie une chaine de caractères
  • Expression<Func<string, string>> représente l’expression permettant de convertir l’objet. Il en existe 2 : une pour l’écriture et une pour la lecture
  • CryptoService représente le service contenant le code de chiffrement/déchiffrement (rappelez-vous, il s’agit du code de l’article d’Emilien). Pour simplifier, j’ai pris l’hypothèse qu’il s’agissait d’une classe statique, mais on peut tout à fait l’injecter via le constructeur ;)

On peut donc désormais remplacer la ligne TODO: Do magic ! de notre méthode OnModelCreating par le code suivant : property.SetValueConverter(new EncryptedConverter());.

Conclusion

A défaut d’avoir accès à la fonctionnalité Always Encrypted de SQL Server, ce mécanisme fait le job et est plutôt simple à mettre en place.

De plus, Entity Framework est suffisamment intelligent pour gérer aussi le chiffrement des paramètres. Imaginons la requête LINQ to Entities suivante : db.Set<MyEntity>.Where(x => x.PrivateData.Contains("test")). EF va automagiquement chiffrer la valeur test avant de générer la requête SQL.

J’espère que cet article vous a appris quelque chose, et que cela pourra vous servir un jour !