Cifrado de datos sensibles en logs (GDPR)
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 JsonConveter
s 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.