A la hora de escribir nuestro código, hagamos como lo hagamos, un hecho irrefutable es que cuando llega a su destino y es ejecutado en el entorno de PRODUCCIÓN, más tarde o más temprano siempre llega el infame día en que nuestro código 'peta'. ¡¡¡OOPS!!!

En este crítico momento, si hemos sido precavidos que, si ya lo has sufrido previamente en tus carnes un par de veces o tres lo eres seguro, nos damos cuenta de que la herramienta fundamental para poder hacer un análisis forense de lo ocurrido son nuestros socorridos logs. Parafraseando a nuestro compañero Alberto "son como el papel higiénico, sólo nos acordamos de él cuando lo necesitamos".

En ese momento, los recuperas y cuando te pones a analizarlos con premura, mientras te tiemblan las piernas al sentirte observado por demasiados pares de ojos impacientes, se te escapa una ligera sonrisa al comprobar aliviado que no hay ningún dato sensible en los logs y que estás cumpliendo con la GDPR en la gestión de los mismos.

¿Hemos hecho magia? ¿Hemos tenido que restructurar todo el proceso de generación de logs? ... No, ha sido algo mucho más fácil ... con un sencillo proceso y usando una pequeña caja de herramientas creada para el cifrado de datos sensibles en los logs, hemos cumplido nuestro objetivo. Vamos a desgranarla ...

Escenario

El punto de partida es:

  • Una implementación propia del interface Common.Logging.ILog del nuget Common.Logging usada para registrar los logs
  • El serializador Newtonsoft.Json.JsonConvert del nuget Newtonsoft.Json, usado para incluir los objetos en los logs

El objetivo es crear un conjunto de herramientas, objetos y utilidades, sencillo de usar, para poder marcar y cifrar la información sensible al usar el serializador indicado.

La caja de herramientas I - Core

En este apartado mostramos los objetos que disponen las aplicaciones y servicios que quieran usar estas herramientas para cifrar los datos sensibles en los logs.

Atributos para marcar la información sensible

Se definen una serie de atributos personalizados para marcar las propiedades de las clases que contienen información sensible

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class LogBaseAttribute : Attribute
{
    public LogBaseAttribute();
} 

public sealed class RecoverableAttribute : LogBaseAttribute
{
    public RecoverableAttribute();
}

public sealed class UnrecoverableAttribute : LogBaseAttribute
{
    public UnrecoverableAttribute();
}

public sealed class LogIgnoreAttribute : LogBaseAttribute
{
    public LogIgnoreAttribute();
}

Métodos para cifrar los datos sensibles

Estos métodos serán los encargados de cifrar objetos:

  • Tipos escalares de datos
  • strings

Se definen métodos específicos para gestionar la información sensible. La implementación de estos métodos define la manera de cifrar la información según las políticas de cifrado de información definidas en la empresa.

class LogHelper
{
    public static string TraceRecoverableValue<T>(T value) // ...
    public static string TraceUnrecoverableValue<T>(T value) // ...
}

Como es evidente existirá un método no especificado para recuperar la información cifrada de manera recuperable

class LogHelper
{
    public static string RecoverTraceValue<T>(T cypherRecoverableData) // ...
}

Los engranajes de la caja de herramientas

En este apartado mostramos los objetos que usan las herramientas para cumplir su cometido.

JsonConverters personalizados

Objetos JsonConverter personalizados para los atributos definidos que invoquen a los métodos de cifrado correspondientes para cada atributo

class RecoverableDataConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) { return true; }

    public override bool CanRead => false;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(LogHelper.TraceRecoverableValue(value));
    }
}
class UnrecoverableDataConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) { return true; }

    public override bool CanRead => false;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(LogHelper.TraceUnrecoverableValue(value));
    }
}

ContractResolver personalizado

Este objeto implementa la interface Serialization.IContractResolver que  analiza los objetos a serializar y decide, en función de los atributos  personalizados con los que esté decorada cada propiedad, si la serializa  usando alguno de los JsonConveters personalizados creados en el punto  anterior.

class LogConverterAttributesContractResolver : DefaultContractResolver
{
    #region Propiedades publicas

    public ConcurrentDictionary<Type, JsonConverter> LogBaseAttributesConverters { get; set; }
    public ConcurrentDictionary<Type, JsonConverter> LogTypesConverters { get; set; }
    public ConcurrentBag<Type> LogBaseAttributesToIgnore { get; set; }

    #endregion

    #region Variables privadas

    private static ConcurrentDictionary<Type, JsonObjectContract> _jsonObjectContracts = new ConcurrentDictionary<Type, JsonObjectContract>();

    #endregion

    #region DefaultContractResolver

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        if (!_jsonObjectContracts.TryGetValue(objectType, out var baseContract))
        {
            baseContract = base.CreateObjectContract(objectType);

            foreach (JsonProperty jsonProperty in baseContract.Properties)
            {
                // Analizamos si la propiedad es de un tipo que tiene un JsonConvert directamente asignado
                var jsonPropertyTypeConverter = default(JsonConverter);
                var propertyHasSpecificJsonConverter = (LogTypesConverters != default(ConcurrentDictionary<Type, JsonConverter>)) &&
                                                       (LogTypesConverters.TryGetValue(jsonProperty.PropertyType, out jsonPropertyTypeConverter));
                if (propertyHasSpecificJsonConverter)
                {
                    jsonProperty.Converter = jsonPropertyTypeConverter;
                }

                // Analizamos si entre los atributos de la propiedad hay algun atributo derivado de LogBaseAttribute
                var propertyInfo = objectType.GetProperty(jsonProperty.UnderlyingName);

                if (propertyInfo != default(PropertyInfo))
                {
                    var logBaseAttributes = propertyInfo.GetCustomAttributes<LogBaseAttribute>();

                    if ((logBaseAttributes != default(IEnumerable<LogBaseAttribute>)) &&
                        (logBaseAttributes.Count() > 0))
                    {
                        // Hemos encontrado atributos LogBaseAttribute. 
                        // Analizamos si hay alguno que indica que hay que ignorar la propiedad en la serialización
                        if ((LogBaseAttributesToIgnore != default(ConcurrentBag<Type>)) &&
                            logBaseAttributes.Any(logBaseAttribute => LogBaseAttributesToIgnore.Contains(logBaseAttribute.GetType())))
                        {
                            jsonProperty.ShouldSerialize = obj => { return false; };
                        }
                        else
                        {
                            // Si la propiedad no tiene asignado ya un JsonConvert en base a su PropertyType analizamos si hay algún atributo con un JsonConverter específico asociado
                            if (!propertyHasSpecificJsonConverter &&
                                (LogBaseAttributesConverters != default(ConcurrentDictionary<Type, JsonConverter>)))
                            {
                                // Buscamos el primer atributo que tenga un JsonConverter específico asociado
                                var logBaseAttributeConverter = logBaseAttributes.FirstOrDefault(logBaseAttribute => LogBaseAttributesConverters.ContainsKey(logBaseAttribute.GetType()));
                                if ((logBaseAttributeConverter != default(LogBaseAttribute)) &&
                                    LogBaseAttributesConverters.TryGetValue(logBaseAttributeConverter.GetType(), out var logBaseAttributteJsonConverter))
                                {
                                    jsonProperty.Converter = logBaseAttributteJsonConverter;
                                }
                            }
                        }
                    }
                }
            }

            // Guardamos el baseContract
            baseContract = _jsonObjectContracts.AddOrUpdate(objectType, baseContract, (type, existingBaseContract) => baseContract);
        }

        return baseContract;
    }

    #endregion
}

Como se puede ver en la implementación se ha tuneado el objeto para que:

  • El análisis por reflexión que se realiza de cada tipo de objetos a serializar sea reutilizado.
  • Se puedan especificar conversores específicos para determinados tipos de propiedades
  • Se puedan indicar los atributos personalizados que no serán incluidos en la serialización

Uso básico de la caja de herramientas

Además de las herramientas ya descritas hay que usar alguna de las siguientes herramientas básicas

La caja de herramientas II - Básicas

class LogHelper
{
    public static void InitializeConvertersAttributes(Formatting jsonFormatting = Formatting.None)
    {
        // ...
        
        // Inicialización de la configuración por defecto de la serialización Json
        JsonConvert.DefaultSettings = () => _jsonSerializerSettingsWithConverterAttributes;
    }
        
    public static void InitializeConvertersAttributesWithCustomTypeConverters(Dictionary<Type, JsonConverter> typesConverters, Formatting formatting = Formatting.None)
    {
        // ...
        
        // Inicialización de la configuración por defecto de la serialización Json
        JsonConvert.DefaultSettings = () => _jsonSerializerSettingsWithConverterAttributes;
    }
}

El uso de las herramientas consiste en:

  • Marcado de las propiedades de las clases que contienen datos sensibles usando los atributos creados.
  • Inicializar la serialización de datos sensible, usando una de las herramientas básicas que se describe a continuación
  • Utilizar el serializador Newtonsoft.Json.JsonConvert del nuget Newtonsoft.Json para incluir los objetos en los logs

Ejemplo de uso de herramientas básicas

Inicialización

// En los procesos de arranque/inicialización usar ...
LogHelper.InitializeConvertersAttributes();

Marcado de propiedades en entidades

// En la definición de las clases marcar las propiedades con datos sensibles con los atributos
public class UserEntityExample
{
    // ...
    
    [Recoverable]
    public string UserName { get; set; }
    
    [Unrecoverable]
    public string Password { get; set; }
    
    [LogIgnore]
    public int Age { get; set; }

    // ...
}

Generación de logs

// Para incluir valores en los logs
var recoverableSensitiveDataValue = LogHelper.TraceRecoverableValue(sensitiveDataValue);
Logger.Debug($"Recoverable sensitive value: {recoverableSensitiveDataValue}");

var unrecoverableSensitiveDataValue = LogHelper.TraceUnrecoverableValue(sensitiveDataValue);
Logger.Debug($"Unrecoverable sensitive value: {unrecoverableSensitiveDataValue}");

// Para incluir objetos en los logs
var exampleUser = new UserEntityExample
{
    // ...
    UserName = "User Name",
    Password = "1234",  // ;-P)
    Age = 46;
    // ...
}
var logeableExampleUser = JsonConvert.SerializeObject(exampleUser);
Logger.Debug($"User Data: {logeableExampleUser}");

En este caso el contenido del log sería:


Recoverable sensitive value: 0X3F ... B4
Unrecoverable sensitive value: 0x29 ... DB

User Data: {
    // ...
    
    "UserName": "0X3F ... B4",
    "Password"; "0x29 ... DB"
    
    // ...
}

Herramientas avanzadas

Las herramientas simples presentadas funcionan correctamente y son perfectamente válidas. Sin embargo, el uso de la inicialización de la configuración por defecto de la serialización Json es algo que puede afectar al comportamiento de otros nugets y/o otros componentes del código que usen el serializador Newtonsoft.Json.JsonConvert del nuget Newtonsoft.Json, junto con clases cuyas propiedades ha sido decoradas con los atributos incluidos en las herramientas básicas.

La caja de herramientas III - Avanzadas

Se ofrecen también herramientas para realizar la serialización Json con el tratamiento de datos sensibles sin tener que inicializar su configuración por defecto.

class LogHelper
{
    #region Variables privadas
        
    private static readonly List<KeyValuePair<Type, JsonConverter>> _logBaseAttributesConverters = new List<KeyValuePair<Type, JsonConverter>>
    {
        new KeyValuePair<Type, JsonConverter>(typeof(RecoverableAttribute), new RecoverableDataConverter()),
        new KeyValuePair<Type, JsonConverter>(typeof(UnrecoverableAttribute), new UnrecoverableDataConverter())
    };
    
    private static readonly JsonSerializerSettings _basicJsonSerializerSettingWithConverterAttributes = new JsonSerializerSettings
    {
        ContractResolver = new LogConverterAttributesContractResolver
        {
            LogBaseAttributesConverters = new ConcurrentDictionary<Type, JsonConverter>(_logBaseAttributesConverters),
            LogBaseAttributesToIgnore = _logBaseAttributesToIgnore
        }
    };
    
private static readonly ConcurrentBag<Type> _logBaseAttributesToIgnore = new ConcurrentBag<Type> { typeof(LogIgnoreAttribute) };
    
    #endregion
        
    #region Metodos públicos
        
    public static string JsonSerializeObjectToLog(object value, Type type, Formatting formatting)

    public static string JsonSerializeObjectToLog(object value, Formatting formatting)

    public static string JsonSerializeObjectToLog(object value)
    {
        return JsonConvert.SerializeObject(value, _basicJsonSerializerSettingWithConverterAttributes);
    }

    public static string JsonSerializeObjectToLog(object value, Formatting formatting, params JsonConverter[] converters)

    public static string JsonSerializeObjectToLog(object value, params JsonConverter[] converters)

    public static string JsonSerializeObjectToLog(object value, Type type)

    #endregion
}

El uso avanzado de las herramientas pasa por sustituir las llamadas al serializador Newtonsoft.Json.JsonConvert del nuget Newtonsoft.Json por las herramientas creadas a tal efecto.

Ejemplo de uso de herramientas avanzadas

Inicialización

// En los procesos de arranque/inicialización no se hace nada

Marcado de propiedades en entidades

Igual que usando las herramientas básicas

Generación de logs

// Para incluir valores en los logs se usan las herramientas básicas
var recoverableSensitiveDataValue = LogHelper.TraceRecoverableValue(sensitiveDataValue);
Logger.Debug($"Recoverable sensitive value: {recoverableSensitiveDataValue}");

var unrecoverableSensitiveDataValue = LogHelper.TraceUnrecoverableValue(sensitiveDataValue);
Logger.Debug($"Unrecoverable sensitive value: {unrecoverableSensitiveDataValue}");

// Para incluir objetos en los logs se usan las nuevas herramientas avanzadas
var logeableExampleUser = LogHelper.JsonSerializeObjectToLog(exampleUser);
Logger.Debug($"User Data: {logeableExampleUser}");

El log generado sería el mismo que usando las herramientas básicas.