A1Force.MauiObfuscate.Build
1.3.2
dotnet add package A1Force.MauiObfuscate.Build --version 1.3.2
NuGet\Install-Package A1Force.MauiObfuscate.Build -Version 1.3.2
<PackageReference Include="A1Force.MauiObfuscate.Build" Version="1.3.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="A1Force.MauiObfuscate.Build" Version="1.3.2" />
<PackageReference Include="A1Force.MauiObfuscate.Build"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add A1Force.MauiObfuscate.Build --version 1.3.2
#r "nuget: A1Force.MauiObfuscate.Build, 1.3.2"
#:package A1Force.MauiObfuscate.Build@1.3.2
#addin nuget:?package=A1Force.MauiObfuscate.Build&version=1.3.2
#tool nuget:?package=A1Force.MauiObfuscate.Build&version=1.3.2
maui-obfuscate
Rename obfuscator for .NET MAUI apps. A dotnet tool plus an MSBuild targets file. It rewrites an already-built assembly (Release DLL + project directory + portable PDB). A Release Android build also rewrites DEX files before the package is signed. It does not need the MAUI workload.
It renames identifiers, flattens control flow, packs string literals, suppresses classic ildasm, adds a de4dot decoy, and stops when a debugger is attached. Each of those can be turned off in maui-obfuscation.json. A dependency with "merge": true is copied into the app. maui-obfuscate deobfuscate reads the rename maps and restores type and method names in a stack trace.
The tool renames each assembly it changes, then writes the obfuscated bytes back to the original file name. The runtime keeps loading App.dll and MyCompany.Core.dll. Beside those files it leaves:
App.dll.originalandApp.pdb.original(the build output, before renaming)App.obfuscated.rename-map.json(deterministic; use it to decode stack traces)
The same pair is written for each dependency that was renamed in place. When that dependency's merge is true, it is copied into the app and that DLL is not written back. The original stays as .original. A later build replaces App.dll and App.pdb; the next obfuscation pass parks that new build as .original again. An Android Release build also leaves classes.dex.original and classes.dex.rename-map.json next to each DEX file, and mapping.txt next to the first dex (copied to the Android output folder for Play Console).
XAML, resx, images, fonts, and other resources are not rewritten. Names those files look up have to stay, which is what the preservation rules below are for.
Install
dotnet tool install only accepts a package whose type is DotnetTool. A project cannot reference a package of that type (NU1212), so the command and the MSBuild import are two packages:
| Package | Install with | Role |
|---|---|---|
A1Force.MauiObfuscate |
dotnet tool install |
The maui-obfuscate command |
A1Force.MauiObfuscate.Build |
PackageReference |
Imports the targets after Build |
dotnet tool install --global A1Force.MauiObfuscate
dotnet tool update --global A1Force.MauiObfuscate
Version 1.0.0 is not a .NET tool. If install stops with Package a1force.mauiobfuscate is not a .NET tool., that version was selected. Install 1.0.2 or later:
dotnet tool install --global A1Force.MauiObfuscate --version 1.0.2
Version 0.0.1 is a tool, and it does not check a licence. Do not use it.
Local manifest instead of a global tool:
dotnet new tool-manifest
dotnet tool install A1Force.MauiObfuscate
A global install is the command maui-obfuscate. It is not dotnet maui-obfuscate. The dotnet host only runs a local tool that way, and otherwise looks for a program named dotnet-maui-obfuscate. The MSBuild package calls the global shim at %USERPROFILE%\.dotnet\tools\maui-obfuscate.exe (or $HOME/.dotnet/tools/maui-obfuscate). For a local tool, set MauiObfuscatorCommand to dotnet tool run maui-obfuscate -- (the -- keeps MSBuild from eating the tool's arguments).
Installing the tool does not change the app project. Reference A1Force.MauiObfuscate.Build for that. See MSBuild below. Do not add a PackageReference to A1Force.MauiObfuscate.
Licence key
Obfuscation requires a licence key. init, --help, and --version do not. A subscription key lasts one year and is issued for one application id. A trial key lasts exactly two days and does not renew. The tool accepts those two lengths and refuses every other length. maui-obfuscate reads <ApplicationId> from the project file and refuses the key when it does not match, so a key cannot be used on another app. Write the id as a literal in the project. A value that exists only in Directory.Build.props is not read. The annual price is USD $1,999.
Set the key in the environment before you build or run the tool. The tool reads A1FORCE_LICENCE_KEY. This keeps the key out of the project file and out of the MSBuild command line.
export A1FORCE_LICENCE_KEY="<key>"
maui-obfuscate --project path/to/App.csproj
On Windows PowerShell:
$env:A1FORCE_LICENCE_KEY = "<key>"
maui-obfuscate --project path/to/App.csproj
A one-off command can pass the key directly:
maui-obfuscate --licence-key "<key>" --project path/to/App.csproj
For an app that references the package, you can set MauiObfuscateLicenceKey instead. The targets pass that property to the tool. Do not commit the key. A value assigned in the project, including $(A1FORCE_LICENCE_KEY), is written into the build log.
<PropertyGroup>
<MauiObfuscateLicenceKey>$(A1FORCE_LICENCE_KEY)</MauiObfuscateLicenceKey>
</PropertyGroup>
Leave that property empty when A1FORCE_LICENCE_KEY is already set. The tool sees the variable on its own.
Init
init writes maui-obfuscation.json next to the project. It does not check a licence key. It does not overwrite an existing file unless you pass --force.
maui-obfuscate init --project path/to/App.csproj
maui-obfuscate init --project path/to/App --settings path/to/maui-obfuscation.json --force
--project may be a .csproj or a directory that contains exactly one.
Each group has:
| Field | Meaning |
|---|---|
name |
Label used when you read the file. |
include |
When true, files in this group are renamed. |
includePatterns |
Globs relative to the project (**, *, ?). * does not cross /. |
excludePatterns |
Globs. A match excludes the file, and it wins over every include, including other groups and defaultInclude. |
Globals:
| Field | Meaning |
|---|---|
defaultInclude |
Files that match no group's include patterns. Shipped default is false. |
explicitTypeIncludes |
Metadata type names (or * globs) to rename when that type has no PDB sequence points. A file-group exclude still wins. |
androidDex |
When true, a Release Android build rewrites DEX after Dalvik compile. init writes true. Set false to leave classes.dex as compiled. |
Decision order for a source file:
- Any
excludePatternsmatch → not renamed. - Otherwise, collect every group whose
includePatternsmatch. - No group matched →
defaultInclude. - Any matched group has
include: false→ not renamed. - Every matched group has
include: true→ renamed.
Patterns are case-insensitive. Paths from the PDB are made project-relative when they sit under the project directory. ** patterns also match a full path when the build machine's prefix does not.
What each default group does
defaultInclude is false, so a file that matches nothing is left alone. Groups with include: false still matter: if you later set defaultInclude to true, those groups stay excluded.
| Group | include | What it matches |
|---|---|---|
| Application | false | MauiProgram.cs, App.xaml.cs, MainActivity.cs, AppDelegate.cs, Platforms/**/Program.cs, Platforms/**/Main.cs |
| Views and XAML code-behind | false | **/*.xaml.cs, **/Views/**/*.cs, **/Pages/**/*.cs |
| ViewModels | true | **/ViewModels/**/*.cs, **/*ViewModel.cs |
| Models | true | **/Models/**/*.cs, **/*Model.cs |
| Services | true | **/Services/**/*.cs, **/*Service.cs |
| Platforms | false | **/Platforms/** |
| Resources | false | Resources, Images, Fonts, Raw, Splash, AppIcon, plus **/*.xaml (not .xaml.cs), resx, and common image/font extensions |
| Generated | false | **/obj/**, **/*.g.cs, **/*.g.i.cs |
| Tests | false | **/Tests/**, **/*Tests/**, **/*Test.cs, **/*Tests.cs, and the *.Test.cs / *.Tests.cs forms |
**/*.xaml does not match Foo.xaml.cs. **/*.g.cs does not match Foo.g.i.cs.
Sample results with the shipped file (true means the file is renamed):
| Path | Renamed | Why |
|---|---|---|
MauiProgram.cs, App.xaml.cs |
no | Application |
Platforms/Android/MainActivity.cs |
no | Application and Platforms |
Platforms/iOS/AppDelegate.cs, Platforms/MacCatalyst/Program.cs, Platforms/Tizen/Main.cs |
no | Application and Platforms |
Views/HomePage.xaml.cs, Views/HomePage.cs, Pages/LoginPage.cs, Custom/Widget.xaml.cs |
no | Views |
ViewModels/LoginViewModel.cs, LoginViewModel.cs |
yes | ViewModels |
Models/User.cs, UserModel.cs |
yes | Models |
Services/AuthService.cs, AuthService.cs |
yes | Services |
Platforms/Android/MainApplication.cs |
no | Platforms |
Platforms/Android/Services/PlatformService.cs |
no | Platforms (include: false) wins over Services |
Resources/Styles/Colors.xaml, images, fonts, splash, app icon |
no | Resources |
obj/.../App.AssemblyInfo.cs, Foo.g.cs, Foo.g.i.cs |
no | Generated |
ViewModels/LoginViewModel.g.cs |
no | Generated wins over ViewModels |
Tests/AuthServiceTests.cs, Services/AuthServiceTests.cs |
no | Tests wins over Services |
Helpers/Format.cs |
no | no group, defaultInclude is false |
ViewModels/Pages/DetailPage.cs |
no | Pages (include: false) wins over ViewModels |
**/*Model.cs also matches LoginViewModel.cs. Both ViewModels and Models default to include: true, so the file is still renamed. If you set Models to include: false, those *ViewModel.cs files drop out too, because exclude wins. Delete the **/*Model.cs pattern if you want ViewModels renamed while the Models group is off. The **/Models/** pattern does not match ViewModels/.
Add a custom group
Edit maui-obfuscation.json. No rebuild. Example: rename Helpers/ and Internal/, but never Helpers/PublicFacade.cs.
{
"name": "InternalHelpers",
"include": true,
"includePatterns": ["**/Helpers/**/*.cs", "**/Internal/**/*.cs"],
"excludePatterns": ["**/Helpers/PublicFacade.cs"]
}
Put it in the groups array. excludePatterns on this group also beat includes from other groups.
Dependencies
dependencies chooses assemblies that sit beside the app output (the copies the app loads) and renames them with the same file groups. The app, and any other selected dependency that calls into them, is rewritten so those calls use the new type and member names. Assemblies that are not listed are left unchanged, and so are references to them.
"dependencies": [
{ "name": "MyCompany.Core", "include": true, "merge": true },
{ "name": "MyCompany.*", "include": true },
{ "name": "ThirdParty.LeaveAlone", "include": false }
]
name is an assembly name, a file name, or a * / ? glob. Matching is case-insensitive. An entry with "include": false wins over a broader include. Omitting include means true. An empty list, the default init writes, renames no dependencies.
Each selected assembly needs its portable PDB beside it. The tool renames MyCompany.Core.dll to MyCompany.Core.dll.original (and the PDB to MyCompany.Core.pdb.original), then writes the obfuscated assembly back to MyCompany.Core.dll. The app is treated the same way. Callers are updated, and they still load MyCompany.Core.dll, which is now the renamed dependency. Deploy that output directory as the build left it. Close the running app before building. A running process keeps those DLLs open, and the rename then fails with "being used by another process".
A glob does not select strong-named framework assemblies. Naming one exactly fails the build, because this tool does not re-sign. A glob also leaves platform assemblies beside the app. That covers names such as Microsoft.Maui.Controls, System.*, Mono.*, Java.*, Android.*, AndroidX.*, Xamarin.*, and ObjCRuntime. Name that assembly exactly to select it. A name that matches nothing beside the output is reported as a warning.
merge is per entry. Set it true to copy that dependency into the app assembly and remove its DLL from the output. Omit it, or set it false, and the DLL stays beside the app. An explicit false wins over a broader true. When the entry names the assembly being obfuscated, merge is ignored. Embedded resources move with the types. Protections run on the app and on each dependency that stays separate. A merged dependency is protected once, inside the app. A dependency that stays separate and calls a merged one is retargeted to the app. An assembly you did not include is not merged, and references to it stay. Mixed-mode assemblies are refused. A framework type the app already defines, such as a generated Microsoft.Maui.Controls type inside a dependency, stays as the app's copy. A type the application owns that is already in the app, including a partial class compiled into both projects, is combined: members that already exist stay as the app's, and members that exist only on the dependency are copied onto that type. The dependency is copied in before renaming. Renaming and the other protections then run once on the merged app assembly, so each namespace gets one set of short names. The rename map for the dependency is still written. When that combine fails, the error names the original class, both assemblies, the source file from the PDB, and the property or method when a member conflicts. A base-class mismatch names both base classes. It is the class declaration, not a property.
Types the PDB cannot place in a file stay unrenamed unless you list them:
"explicitTypeIncludes": ["MyApp.Internal.GeneratedMarker", "MyApp.Internal.*"]
Run
Build the MAUI app first (Release, with a portable PDB). Then:
maui-obfuscate --project path/to/App.csproj --configuration Release --settings maui-obfuscation.json
Before it rewrites an assembly, the command prints this build's version and the A1Force.MauiObfuscate.Build version from the project file. Obfuscation runs only when that PackageReference version is the same literal as this build. A missing reference, a version written as an MSBuild property, or a different version stops the run before any assembly is rewritten. A target framework or runtime identifier that has no built assembly is skipped with a warning. The assemblies that were built are obfuscated.
Useful options:
| Option | Default | Role |
|---|---|---|
--project |
required | .csproj or directory |
--configuration |
Release |
Used to find bin/<configuration>/<tfm>/<AssemblyName>.dll |
--settings |
<project>/maui-obfuscation.json |
Relative paths are resolved from the current directory |
--assembly |
discovered from the project | Skip discovery and obfuscate this DLL. The MSBuild target passes $(TargetPath) |
--framework |
all TFMs that have a DLL | Limit discovery to one target framework. A requested framework with no DLL is skipped with a warning. |
Multi-targeted output: one preferred DLL per folder under bin/<configuration>/ (the shortest path in that folder). Reference assemblies under ref/ are skipped. A declared target framework or runtime identifier with no DLL is reported as a warning and left alone. Pass --assembly when the app's real output is not that layout. The MSBuild target does the same when $(TargetPath) is not on disk.
The app project must already build. This tool does not invoke dotnet build and does not need the MAUI workload.
Android DEX
A Release Android build obfuscates DEX as well as the .NET assembly when androidDex is true (the default). Set "androidDex": false in maui-obfuscation.json to skip that pass and leave classes.dex as compiled. MauiObfuscateDex runs after _CompileToDalvik and before the APK or app bundle is signed. The signed package is not edited. The .NET pass still runs after Build.
maui-obfuscate dex --project path/to/App.csproj --dex path/to/classes.dex
Omit --dex and the command scans obj and bin for *.dex when the target framework contains -android. A normal maui-obfuscate run does the same after the assemblies. The first run copies each file to *.dex.original. A later run reads that backup. A compile that replaces the dex is obfuscated from the new bytes.
App classes whose hierarchy is only java.lang.Object and other app classes are renamed. Android, AndroidX, Kotlin, Java, Mono, and crc callable-wrapper classes keep their names, as do activities, enums, annotations, and any class that extends a platform type. Private methods and fields on those kept classes are still renamed. Constructors, native methods, values, and valueOf stay. Debug info and source file names are stripped.
The map is classes.dex.rename-map.json. The kinds are DexClass, DexMethod, and DexField. Stack-trace deobfuscation ignores those kinds. Leave the map and *.dex.original out of the package you upload.
The DEX pass also writes mapping.txt next to the first dex. The MSBuild target copies it to $(OutputPath). That file is a ProGuard mapping Play Console accepts for Java and Kotlin crash traces: original -> obfuscated: class lines, then indented field and method lines. Upload it on the release as a deobfuscation file. Do not put it in the app bundle. If R8 already wrote proguard/mapping.txt under the Android intermediate output, that file is composed in so the left-hand names stay the source names and the right-hand names match the shipped dex. A second run composes with that R8 file, not with the previous mapping.txt, so names are not applied twice.
MSBuild
Reference the MSBuild package from the application project:
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="A1Force.MauiObfuscate.Build" Version="1.0.2" />
</ItemGroup>
</Project>
NuGet imports build/A1Force.MauiObfuscate.Build.targets from that package. The file name matches the package id, which is the rule NuGet uses, so the project does not need an Import.
A1Force.MauiObfuscate.Build 1.0.1 runs dotnet maui-obfuscate. That does not start a global tool, and the build fails with dotnet-maui-obfuscate does not exist. Version 1.0.2 calls the shim directly. On 1.0.1, set the command on the app:
<PropertyGroup>
<MauiObfuscatorCommand>$(USERPROFILE)\.dotnet\tools\maui-obfuscate.exe</MauiObfuscatorCommand>
</PropertyGroup>
On macOS and Linux that path is $(HOME)/.dotnet/tools/maui-obfuscate.
Do not reference A1Force.MauiObfuscate from the app. From 1.0.1 it is a DotnetTool package, and NuGet reports NU1212.
The reference does not install the maui-obfuscate command. Install the tool as well (see Install). The imported target runs after Build, and only for Release, after a real build (design-time builds are skipped). Set A1FORCE_LICENCE_KEY in that build's environment.
A Release Android target framework also runs MauiObfuscateDex after _CompileToDalvik when androidDex is true. That rewrites DEX before the package is signed. See Android DEX above.
| Property | Default | Role |
|---|---|---|
MauiObfuscateAfterBuild |
true |
Set false to skip the target |
MauiObfuscationSettings |
$(MSBuildProjectDirectory)/maui-obfuscation.json |
--settings |
MauiObfuscateConfiguration |
$(Configuration) |
--configuration |
MauiObfuscatorCommand |
global shim, else dotnet maui-obfuscate |
Full path of maui-obfuscate.exe when the global tool is installed. Local tool: dotnet tool run maui-obfuscate -- |
MauiObfuscateAssembly |
$(TargetPath) |
--assembly |
MauiObfuscateOnAnyConfiguration |
false |
When true, also run outside Release |
MauiObfuscateLicenceKey |
empty | Passed as --licence-key. Leave empty and set A1FORCE_LICENCE_KEY so the key stays out of the log |
$(TargetPath) is the assembly the build just produced, which is the right input for a multi-targeted MAUI app.
Azure DevOps
A Release build on the agent runs the tool when the global command is installed and A1FORCE_LICENCE_KEY is in that step's environment. The agent still needs the .NET SDK and the MAUI workload the app already builds with.
Add A1FORCE_LICENCE_KEY as a secret variable on the pipeline. A secret is not an environment variable until the build step maps it. Map it on the step that runs dotnet build. Do not pass MauiObfuscateLicenceKey. That property is written into the build log.
Leave the PackageReference version as a literal in the project file. Set obfuscatorVersion in the pipeline to that same number. A version written as an MSBuild property is refused before any assembly is rewritten.
trigger:
- main
pool:
vmImage: windows-latest
variables:
buildConfiguration: Release
# Same literal as PackageReference Version for A1Force.MauiObfuscate.Build.
obfuscatorVersion: 1.2.4
steps:
- task: UseDotNet@2
displayName: Use the SDK the app targets
inputs:
packageType: sdk
version: 8.x
- script: dotnet tool install --global A1Force.MauiObfuscate --version $(obfuscatorVersion)
displayName: Install maui-obfuscate
- script: dotnet build src/App/App.csproj --configuration $(buildConfiguration)
displayName: Build Release
env:
A1FORCE_LICENCE_KEY: $(A1FORCE_LICENCE_KEY)
- powershell: |
$dest = "$(Build.ArtifactStagingDirectory)/rename-maps"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Get-ChildItem -Path src/App -Recurse -Include *.obfuscated.rename-map.json,*.dex.rename-map.json |
Copy-Item -Destination $dest
Get-ChildItem -Path src/App/bin -Recurse -Filter mapping.txt |
Copy-Item -Destination "$dest/mapping.txt"
displayName: Collect rename maps
- publish: $(Build.ArtifactStagingDirectory)/rename-maps
artifact: rename-maps
displayName: Publish rename maps
Change 8.x to the SDK the project targets, and change src/App/App.csproj to the application project. Drop the UseDotNet step when the hosted image already has that SDK and the MAUI workload. dotnet tool install --global writes maui-obfuscate.exe under the agent user's profile, which is where the imported target looks. On ubuntu-latest the same pipeline works; the shim is $HOME/.dotnet/tools/maui-obfuscate.
The rename-map artifact is for deobfuscate. DEX maps from an Android build are collected with them. mapping.txt from the Android output folder is the ProGuard file Play Console accepts. Upload that file on the release as a deobfuscation file. Leave those files out of the app package you ship. The licence key is not an artifact.
What is preserved, and why
The tool renames types and members whose source file is included. It does not edit XAML or resources, so anything MAUI or the XAML loader looks up by name has to keep that name or the app will not start.
Always kept:
- Type names
MauiProgram,App,MainActivity,AppDelegate,Program,MainApplication, and whatever type actually holds the assembly entry point. - Methods named
CreateMauiAppandMain, and the entry-point method itself. - The
x:Classtype and its public surface (properties, fields, methods, events). Code-behind is whatInitializeComponentand the resource loader bind to. Private members on that type can still be renamed. x:Namemembers on thatx:Classtype, including private fields the XAML generator assigns.- Public properties — and every other member — of a type whose source file is excluded. Bindings and other XAML that target an excluded page or model keep working because those names are not rewritten.
[System.Reflection.Obfuscation(Exclude = true)].Excludedefaults totruewhen the attribute is present.ApplyToMembersdefaults totrue, so a type-level attribute also keeps the members. The same attribute on the assembly keeps the whole assembly.- Virtual methods, overrides, and interface methods, so the runtime still binds
OnCreate,OnAppearing,INotifyPropertyChanged, and anything else implemented against the framework. - P/Invoke and
UnmanagedCallersOnlymethods. - Enum field names, constructors, and compiler-generated names that contain
<. - Android, AndroidX, Kotlin, Java, Mono, and
crcDEX classes, plus any DEX class that extends a platform type, including activities and enums. DEX constructors, native methods,values, andvalueOfstay. Private methods and fields on a kept DEX class are still renamed.
A type is renamed only when every PDB sequence-point file for that type is included. One excluded partial file keeps the type name. A member with its own sequence points follows those files. A member with none (typical fields) follows the type's file decision.
Types with no sequence points are not renamed unless explicitTypeIncludes lists them. An excluded file still wins over that list.
PDB
File groups work by mapping IL back to a source file through PDB sequence points. Build with:
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
The SDK already does this for net8.0, including Release. The .pdb must sit beside the DLL and match it. If it is missing or stale, the tool stops. It will not pretend the assembly was renamed.
Rename map
App.obfuscated.rename-map.json lists every rename, sorted so the same input always produces the same file:
{
"assembly": "App.dll",
"renames": [
{
"kind": "Type",
"original": "MyApp.Services.AuthService",
"obfuscated": "MyApp.Services.a"
},
{
"kind": "Method",
"original": "MyApp.Services.AuthService::SignIn(System.String)",
"obfuscated": "MyApp.Services.a::b"
}
]
}
kind is Type, Method, Field, Property, or Event. Search the obfuscated value when a stack trace shows the short name. Names are assigned from a fixed letter sequence in sorted full-name order, skipping any name that is still in use in that scope. They do not depend on the clock.
A DEX map is classes.dex.rename-map.json. Its kinds are DexClass, DexMethod, and DexField. deobfuscate ignores those kinds. The Play Console file is mapping.txt next to the dex. See Android DEX above.
Stack traces
maui-obfuscate deobfuscate rewrites a stack trace with those maps. It requires the same licence and <ApplicationId> as obfuscation. The restored trace goes to standard output. The file list goes to standard error.
maui-obfuscate deobfuscate --project path/to/App.csproj --map path/to/App.obfuscated.rename-map.json --stack trace.txt
Pass --map once for each map, or pass the directory that contains the *.obfuscated.rename-map.json files. Omit --stack to read the trace on standard input. A frame the maps do not name is left as it was. A short name that appears in more than one map, with two different originals, is left unchanged and reported.
An obfuscation run prints the files a later deobfuscation needs:
- every
*.obfuscated.rename-map.jsonwritten for that run, including a dependency that was merged into the app - the command to run, with
--project,--map, and--stack
The obfuscated DLL is not read. Each .pdb.original file is the PDB from before this pass, and deobfuscation does not read it. Flattening moves the IL, and the obfuscated assembly is written without a PDB, so an in file:line suffix stays as it appeared in the trace. Keep the maps where you can reach them, and leave them out of the package you upload.
Protections
init writes these on and flattens once. Set any flag to false to skip that pass.
"protections": {
"controlFlowFlattening": true,
"controlFlowFlatteningIterations": 1,
"packing": true,
"antiDebug": true,
"antiIldasm": true,
"antiDe4dot": true
}
Control-flow flattening rewrites a method into a dispatcher: each basic block sets a state and jumps back to a switch. The method still returns the same values. controlFlowFlatteningIterations repeats that rewrite. Omit it and each method is flattened once. The value must be from 1 to 8. A later pass that flattens nothing stops early. Each extra pass wraps the previous dispatcher and grows the method. Methods that contain an exception handler, a pinned local, or a branch that leaves a value on the stack are left unchanged, so try/catch and ?. stay valid. This is ordinary IL, so MAUI AOT (including iOS) can still compile it.
Packing replaces ldstr with a call that inflates a compressed table and decodes the string. The plaintext is not stored in the assembly. XAML, resx, images, fonts, and other resources are not packed. The same AOT limit applies: the decoder is a normal method, not a native unpacker.
Anti-debug adds a module initializer that calls Debugger.IsAttached and, when that is true, Environment.FailFast. It uses the portable managed check, so the same assembly runs on Windows, Mac, Linux, Android, and iOS. Close the debugger before launching a protected build. The check is at startup only.
Anti-ILDASM applies SuppressIldasmAttribute to the module and the assembly. Classic ildasm.exe stops. ILSpy, dnSpy, and the ildasm in current .NET SDKs do not honor it. The attribute is obsolete in .NET 6 and later and does not change how the CLR runs the assembly.
Anti-de4dot defines ConfusedByAttribute and DotfuscatorAttribute and applies both to the module and the assembly. A default de4dot run sees the Confuser signature and the Dotfuscator signature and stops. A tool that ignores those names still opens the file. The attributes are not executed unless something reflects over them. An existing type of that name is reused when it has a parameterless constructor.
Limits
- Flattening does not rewrite methods with exception handlers or a non-empty stack at a branch. Iterations above 1 repeat the rewrite and grow the IL. The maximum is 8.
SuppressIldasmAttributeasks classic ildasm to stop. Other decompilers ignore it.- The de4dot decoy is a signature collision. It does not stop a tool that ignores
ConfusedByAttributeandDotfuscatorAttribute. - A dependency with
mergetrue is removed after parking.original.mergefalse, or omittingmerge, leaves that DLL in place. An explicit false wins over a broader true.mergeis ignored for the assembly being obfuscated. Strong-named and mixed-mode assemblies are refused. - Resources, XAML, resx, images, and fonts are not rewritten. A string in XAML, a binding path, or a reflection lookup of an old name is not updated.
- Public properties on included types are renamed. Default settings include ViewModels, so
{Binding UserName}on an included view model breaks until you set that group toinclude: false, narrow its patterns, or mark the property with[Obfuscation(Exclude = true)]. Properties on excluded types are kept. - Virtuals, interface implementations, and enum field names are kept so the app still binds to the framework. They are not scrambled.
- Strong-named assemblies are refused. The tool does not re-sign.
ldstrof a type or member name is not rewritten.- The tool does not build the app and does not install the MAUI workload. Point it at a DLL that is already built.
- The output has no PDB. Decode stacks with the rename map.
- Discovery of
bin/<configuration>/<tfm>/*.dlldoes not evaluate MSBuild conditions. The targets file passes$(TargetPath), which avoids that. - DEX renaming does not touch Android framework classes, callable wrappers, or a class that extends a platform type. The signed APK is not edited. Debug info is dropped. A second run reads
*.dex.original.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.3.2 | 26 | 10/29/2026 |
| 1.3.1 | 29 | 10/29/2026 |
| 1.3.0 | 51 | 10/28/2026 |
| 1.2.9 | 42 | 10/28/2026 |
| 1.2.8 | 47 | 10/28/2026 |
| 1.2.7 | 40 | 10/28/2026 |
| 1.2.6 | 48 | 10/28/2026 |
| 1.2.5 | 48 | 10/27/2026 |
| 1.2.4 | 47 | 10/27/2026 |
| 1.2.3 | 76 | 10/26/2026 |
| 1.2.2 | 69 | 10/26/2026 |
| 1.2.1 | 81 | 10/26/2026 |
| 1.2.0 | 82 | 10/26/2026 |
| 1.1.0 | 89 | 10/24/2026 |
| 1.0.3 | 86 | 10/23/2026 |
| 1.0.2 | 89 | 10/23/2026 |
| 1.0.1 | 90 | 10/23/2026 |