ShieldySdk 1.0.1
dotnet add package ShieldySdk --version 1.0.1
NuGet\Install-Package ShieldySdk -Version 1.0.1
<PackageReference Include="ShieldySdk" Version="1.0.1" />
<PackageVersion Include="ShieldySdk" Version="1.0.1" />
<PackageReference Include="ShieldySdk" />
paket add ShieldySdk --version 1.0.1
#r "nuget: ShieldySdk, 1.0.1"
#:package ShieldySdk@1.0.1
#addin nuget:?package=ShieldySdk&version=1.0.1
#tool nuget:?package=ShieldySdk&version=1.0.1
Shieldy .NET SDK
The .NET client for Shieldy's native runtime (native.dll on Windows, native.so on Linux). It
signs your application in with the end user's license key and reads what the dashboard delivers to
that license: the license itself, remote variables, files and Protected Strings.
using ShieldySdk;
try
{
Shieldy.Start(new ShieldyConfig("<App GUID>", "1.0"), ReadLicenseKeyFromUser());
}
catch (ShieldyException e)
{
// e.Error is a ShieldyError, e.g. ShieldyError.AuthenticationFailed
ShowError("Could not sign in: " + e.Error);
return;
}
ShieldyLicense license = Shieldy.GetLicense();
if (license.Level >= 20) EnableProFeatures();
string apiUrl = Shieldy.GetVariableById("<Lookup ID>"); // string
int maxUsers = Shieldy.GetVariableById<int>("<Lookup ID>"); // int, long, double, bool
using (SecureBuffer pak = Shieldy.GetFileById("<Lookup ID>")) // wiped when disposed
{
LoadAssets(pak.OpenRead());
}
string token = Shieldy.GetProtectedString("<ciphertext>"); // string
One package, ShieldySdk, for .NET Framework 4.6.2 and later, every .NET from 8 up, and
.NET Standard 2.0. The only dependency is Bouncy Castle
(BouncyCastle.Cryptography, MIT).
Installing
dotnet add package ShieldySdk # once published
dotnet add reference path\to\ShieldyDotnetSdk\src\ShieldySdk\ShieldySdk.csproj # from a checkout
Ship the runtime with your application. Download it from the dashboard (your app → Activity → Runtime → "Download the SDK") and put it next to your executable, for example:
<ItemGroup>
<None Include="native\native.dll" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Compatibility
| Your application | Works | The package gives it |
|---|---|---|
| .NET Framework 4.6.2, 4.7.x, 4.8, 4.8.1 (WinForms, WPF, console, services) | yes | lib/net462 |
| .NET 8, 9, 10 and later | yes | lib/net8.0 (trimming and Native AOT annotated) |
| .NET Core 3.1, .NET 5, 6, 7 | yes, not tested (out of Microsoft support) | lib/netstandard2.0 |
| Mono, Unity (Mono backend), other .NET Standard 2.0 hosts | best effort, not tested | lib/netstandard2.0 |
| .NET Framework 4.0 - 4.6.1 | no | out of Microsoft support since 2022; retarget to 4.6.2 (usually one line, the app keeps running on the same machines) |
| Toolchain | Works |
|---|---|
| Visual Studio 2017 or later, MSBuild 15+, any .NET SDK | yes |
PackageReference and packages.config |
yes |
| C# 7.3 and later in your project | yes (the SHIELDY_OBF file is written in C# 3) |
| Processor | Ship |
|---|---|
| x64 process | the 64-bit runtime |
| x86 process | the 32-bit runtime |
Match the runtime's bitness to the process. Old WinForms and WPF projects are often "AnyCPU" with
"Prefer 32-bit" on, which makes the process 32-bit even on 64-bit Windows. Loading a 64-bit
native.dll into it fails with NativeLibraryNotFound and a message that says so; set
<PlatformTarget>x64</PlatformTarget> (or turn "Prefer 32-bit" off), or ship the 32-bit runtime.
Where the runtime is loaded from
The SDK loads exactly one file, chosen in this order:
ShieldyConfig.NativeLibrary, if you set it;- the
SHIELDY_NATIVE_LIBRARYenvironment variable; native.dll(native.soon Linux).
A relative path is resolved against your application's directory (AppContext.BaseDirectory),
never the current working directory or PATH, so a library planted where the user starts your
program is not picked up. On Windows the runtime's own dependencies are searched only next to it and
in System32. Shieldy.ApplicationDirectory and Shieldy.ResolveNativeLibrary() show what the SDK
settled on.
- Single-file publish (
PublishSingleFile): keepnative.dllnext to the executable, not inside the bundle (<ExcludeFromSingleFile>true</ExcludeFromSingleFile>on its item). - Trimming and Native AOT: the
net8.0build is marked trimmable and AOT-compatible and is clean under the trim and AOT analyzers: the runtime is called through unmanaged function pointers, with no reflection or runtime-generated marshalling. A Native AOT publish itself has not been tested yet.
API
Every call is a static method on Shieldy, the one session of the process. All are thread-safe.
| Call | Returns |
|---|---|
Init(config) |
Loads the runtime and runs its protected start-up (/initialize on the server). |
Login(key) |
Signs in with the license key. |
Start(config, key) |
Init then Login. |
TryInit, TryLogin, TryStart |
The same, with out ShieldyError instead of an exception. |
Shutdown() |
Ends the session. Also runs when the process exits. |
IsLoggedIn |
bool |
Supports(ShieldyFeature.LookupIds) |
Whether the runtime has the *ById calls (protocol 1.6.0+). |
Supports(ShieldyFeature.OfflineStart) |
Whether the runtime has GetConnectionState() (protocol 1.12.0+). |
GetConnectionState() |
ShieldyConnectionState: IsOffline (this run started without a connection), OfflineUntil (until when the device can start offline; null: it cannot). |
GetLicense() |
ShieldyLicense: Level, HwidSeatsUsed, HwidSeatsTotal, Created, Expires (null: never), IsExpired, DaysLeft. Read once at login. |
GetVariableById(id) / GetVariable(name) |
string |
GetVariableById<T>(id) / GetVariable<T>(name) |
string, bool, int, long or double |
GetFileById(id) / GetFile(name) |
SecureBuffer, wiped when disposed |
GetProtectedString(ciphertext) |
string, cached for the rest of the session |
Prefer the *ById calls: a lookup id is random, a name ends up readable in your assembly. A
variable whose name is hidden from the client (dashboard: "Hide name from client") can only be read
by id.
bool variables accept true, false, 1, 0. Numbers follow the same rules in every Shieldy
SDK: surrounding whitespace is ignored; a leading +, digit separators, hex and culture formats
(1,5) are not accepted. The current culture never matters.
Errors
Every call throws ShieldyException when it cannot deliver; e.Error says why, and for a runtime
that cannot be loaded e.Message says where it looked. The numbers are the same in the C++ and
Python SDKs.
ShieldyError |
Meaning |
|---|---|
NativeLibraryNotFound |
No runtime at the resolved path, or one for the other bitness. |
NativeLibraryInvalid |
The file loaded but is not a Shieldy runtime. |
InitializationFailed |
The server refused to start the app: unknown App GUID, suspended app, outdated version, unregistered runtime build, maintenance. |
AuthenticationFailed |
The key was refused: wrong, expired, banned, or no free device seat. The server does not say which. |
NotInitialized, NotLoggedIn |
A call came too early. |
NotFound |
No variable with that id or name for this license. |
NativeTooOld |
The runtime predates the call; see Supports(). |
AttestationFailed, KeyExchangeFailed, DecryptFailed |
The runtime could not prove it holds the session key, or a value did not decrypt. Treat as tampering. |
InvalidValue |
A variable did not parse as the requested type. |
ServerUnreachable |
No connection, and this device cannot start offline (offline start is off, it never signed in online here, or that was with another key). |
OfflineExpired |
No connection, and this device's offline time (or the license) has run out. |
ClockInvalid |
No connection, and the system clock reads earlier than the last run. |
OfflineUnavailable |
The call needs a connection and the app is running offline (files). |
The full list, with numbers, is in src/ShieldySdk/ShieldyError.cs.
Offline start
When the app has offline start on (dashboard: Security → Availability), a device that signed in
online can start without internet for the number of days the app allows. Init and Login
succeed offline as they do online, so nothing has to change. To show it:
ShieldyConnectionState state = Shieldy.GetConnectionState();
if (state.IsOffline)
ShowBanner($"Offline - works until {state.OfflineUntil:MMM d}");
Offline, the license, variables and Protected Strings work; files need a connection. The device
has to sign in with the key it last used online. For a portable install, set
new ShieldyConfig(...) { LeaseDirectory = "leases" }. To test without pulling the cable, set
SHIELDY_DEV_FORCE_OFFLINE=1 with a development build of the runtime.
Trying another key
When a key is refused, Login() can be called again with another one, and Init() works again after
Shutdown(). Runtimes built before October 2026 allowed one sign-in per process (the server ends the
session at a refused key and they could not open another); with one of those, restart the process to
let the user try another key.
Files and memory
GetFileById() returns a SecureBuffer: the decrypted bytes in one array that is pinned for its
whole life (so the garbage collector never leaves a moved copy behind) and zeroed by Dispose() or,
if you forget, by the finalizer. Read it through the buffer (OpenRead(), the indexer, CopyTo(),
AsSpan() on .NET 8+); ToArray() makes a copy the SDK cannot wipe. This is best effort, as it has
to be in .NET: copies your own code or a library makes (a string from GetString(), for instance)
are out of the SDK's reach.
Protected Strings in the build
Write secrets as SHIELDY_OBF("...") (a regular string literal) and keep coding against the plain
text. The package adds the marker to every C# project that references it:
using ShieldySdk;
using static ShieldySdk.ShieldyObfMarker;
static string ApiEndpoint() { return SHIELDY_OBF("https://api.example.com/v2"); }
Before a release, run the dashboard's CLI over each file. It rewrites every marker to
Shieldy.GetProtectedString("<ciphertext>"):
python tools/shieldy_strings_cli.py --api-url https://<dashboard>/api --app-guid <App GUID> `
--api-key <key with apps:write> --file src/Endpoints.cs --language csharp --in-place
A marker left in the code does not compile (error CS0619), the same guarantee as the C++ SDK's
static_assert, so a literal you meant to protect cannot ship in the clear. For debug builds define
SHIELDY_OBF_PLAINTEXT and the marker returns the literal itself:
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>$(DefineConstants);SHIELDY_OBF_PLAINTEXT</DefineConstants>
</PropertyGroup>
Never define it for a release build. Verbatim (@"..."), interpolated ($"...") and concatenated
literals are not rewritten by the CLI; they stay markers and fail the build. Read protected strings
where they are used, after Login(), never in a static field initializer. To leave the marker out
of a project, set <ShieldyObfMarker>false</ShieldyObfMarker>. See
examples/ProtectedStringsMarker.
Examples
examples/Basic: the whole flow, configured from environment variables (SHIELDY_APP_GUID,SHIELDY_LICENSE_KEY, optionalSHIELDY_VARIABLE_ID,SHIELDY_FILE_ID,SHIELDY_PROTECTED_STRING). Builds for .NET Framework 4.8 and .NET 10.examples/ProtectedStringsMarker: theSHIELDY_OBFworkflow. Its Release build fails until the CLI has run, on purpose, which is why it is not in the solution.
Known interactions
- Bouncy Castle. The SDK depends on
BouncyCastle.Cryptography2.x. An application that also references the oldPortable.BouncyCastlepackage gets both, and the two declare the sameOrg.BouncyCastletypes; move it toBouncyCastle.Cryptography, its successor. InternalsVisibleTobetween two of your projects that both reference the SDK makes the compiler seeShieldyObfMarkertwice (warning CS0436, harmless). Set<ShieldyObfMarker>false</ShieldyObfMarker>in the project that does not use the marker.
Working on this repository
dotnet build ShieldySdk.sln
dotnet test tests/ShieldySdk.Tests # unit tests on net462, net8.0, net10.0 against a mock runtime
dotnet test tests/ShieldySdk.E2E # a real runtime against a real server, see its README
powershell -File scripts/check-consumers.ps1 # pack, then build customer-style projects from the .nupkg
dotnet pack src/ShieldySdk -c Release -o artifacts/feed
How the SDK works inside, why it is built the way it is, and what to touch for a change: docs/architecture.md. Orientation for an AI assistant (and a quick map for anyone): CLAUDE.md.
third_party/shieldy_api.h is a copy of ShieldyCore's cpp-module/shieldy_api.h, the ABI the SDK
mirrors in src/ShieldySdk/Internal/Abi.cs. AbiTests fails when the two disagree, and when the
copy differs from a ShieldyCore checkout next to this repository.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- BouncyCastle.Cryptography (>= 2.7.0)
-
.NETStandard 2.0
- BouncyCastle.Cryptography (>= 2.7.0)
-
net8.0
- BouncyCastle.Cryptography (>= 2.7.0)
GitHub repositories
This package is not used by any popular GitHub repositories.