Proteggere i segreti con la cifratura di stringhe e valori
Quasi ogni applicazione contiene segreti nel proprio codice: chiavi API, endpoint, stringhe di connessione, parametri di licenza, numeri “magici” e tabelle di ricerca. Un decompilatore li legge in pochi secondi. Questo esempio usa il pacchetto NuGet di Babel Obfuscator per rimuovere questi valori letterali dall’assembly distribuito con la cifratura delle stringhe e la cifratura di valori e array, senza cambiare una riga di codice.
Per usare questo esempio ti serve una licenza per sede di Babel Obfuscator (edizioni Ultimate, Server o Data Center). Il codice sorgente è disponibile su GitHub:
git clone https://github.com/babelfornet/secret-protection-nuget-example.gitIl progetto console SecretApp fa riferimento al pacchetto NuGet di Babel Obfuscator (usato solo per
la build) e attiva i due strati di cifratura in 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>L’algoritmo per le stringhe stream
sposta ogni valore letterale stringa in una tabella cifrata, mentre la
cifratura di valori e array rimuove dall’IL le costanti
numeriche e gli array inline. Entrambi vengono decifrati su richiesta in fase di esecuzione, quindi
il programma si comporta esattamente come prima mentre le costanti scompaiono dal file binario.
Impostare BabelWarningsAsErrors su W00000 trasforma in errore una build in modalità demo, così un
file binario non protetto non può mai essere rilasciato per sbaglio.
I segreti si trovano in 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 };
}L’esempio mette in evidenza anche un dettaglio importante: i segreti sono dichiarati
static readonly, non const. Un valore const viene scritto nei metadati dell’assembly e
copiato inline in ogni punto di utilizzo, quindi sopravvive all’offuscamento e resta leggibile; un
campo static readonly viene assegnato con un’istruzione ldstr/ldc che la cifratura delle
stringhe e dei valori riscrive. Compilando l’esempio in Release e cercando nei due assembly si
conferma la differenza: la chiave API, l’endpoint e la stringa di connessione sono presenti nella
build Debug e assenti dalla build Release, con un output del programma identico.