Protección de secretos con cifrado de cadenas y valores
Casi todas las aplicaciones se distribuyen con secretos en el código: claves API, puntos de acceso, cadenas de conexión, parámetros de licencia, números «mágicos» y tablas de búsqueda. Un descompilador los lee en segundos. Este ejemplo usa el paquete NuGet de Babel Obfuscator para eliminar esos literales del ensamblado distribuido con el cifrado de cadenas y el cifrado de valores y matrices, sin cambiar una sola línea de código.
Para usar este ejemplo necesita una licencia de sitio de Babel Obfuscator (ediciones Ultimate, Server o Data Center). El código fuente está disponible en GitHub:
git clone https://github.com/babelfornet/secret-protection-nuget-example.gitEl proyecto de consola SecretApp hace referencia al paquete NuGet de Babel Obfuscator (solo para la
compilación) y activa las dos capas de cifrado en Release:
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="Babel.Obfuscator" Version="12.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<StringEncryption>stream</StringEncryption>
<ValueEncryption>int32=true;int64=true;single=true;double=true;array=true;true</ValueEncryption>
<ControlFlowObfuscation>if=true;switch=true;case=true;chain=true;true</ControlFlowObfuscation>
<BabelWarningsAsErrors>W00000</BabelWarningsAsErrors>
</PropertyGroup>El algoritmo de cadenas stream
traslada todos los literales de cadena a una tabla cifrada, mientras que el
cifrado de valores y matrices elimina del IL las constantes
numéricas y las matrices en línea. Ambos se descifran a petición en tiempo de ejecución, de modo que el
programa se comporta exactamente igual que antes mientras las constantes desaparecen del binario.
Asignar W00000 a BabelWarningsAsErrors convierte en error una compilación en modo de evaluación, de
modo que un binario sin proteger nunca puede distribuirse por accidente.
Los secretos están en src/SecretApp/Secrets.cs:
internal static class Secrets
{
// Strings -> removed by String Encryption (stream)
public static readonly string ApiKey = "DEMO-API-KEY-0000-1111-2222-3333-4444-5555";
public static readonly string ServiceEndpoint = "https://api.internal.example.com/v3/ingest";
public static readonly string ConnectionString = "Server=db.internal;Database=Orders;User Id=svc;Password=__DEMO_PLACEHOLDER__;";
// Numbers -> removed by Value Encryption
public static readonly int ApiPort = 8443;
public static readonly double RiskThreshold = 0.8734;
// Lookup table -> removed by Array Encryption
public static readonly int[] RolloutBuckets = { 12, 47, 63, 88, 91, 128, /* … */ 8192, 9001 };
}El ejemplo pone de relieve además un detalle importante: los secretos se declaran como
static readonly, no como const. Un valor const queda grabado en los metadatos del ensamblado
y se inserta en línea en cada lugar donde se usa, de modo que sobrevive a la ofuscación y sigue siendo
legible; un campo static readonly se asigna con una instrucción ldstr/ldc que el cifrado de
cadenas y de valores reescribe. Compilar el ejemplo en Release y buscar en los dos ensamblados confirma
la diferencia: la clave API, el punto de acceso y la cadena de conexión están presentes en la
compilación Debug y han desaparecido de la compilación Release, con una salida del programa idéntica.