A .NET library that provides automatic encoding/decoding of numeric properties to obfuscated strings during JSON serialization using attributes. This helps prevent exposing internal numeric IDs in APIs while maintaining clean, readable code.
- Attribute-based: Simply mark properties with
[Cloak]to enable encoding - Automatic JSON conversion: Properties are automatically encoded to strings during serialization and decoded back during deserialization
- Pluggable encoding: Support for different encoding strategies (Sqids provided out of the box)
- Dependency injection: Full integration with Microsoft.Extensions.DependencyInjection
- Type safety: Compile-time type checking with support for all numeric types
- Nullable support: Full support for nullable numeric types
While CloakId is not a substitute for proper security measures like authentication, authorization, and access control, it provides valuable protection against information disclosure and business intelligence gathering.
The German Tank Problem: During WWII, Allied forces estimated German tank production by analyzing captured tank serial numbers. By observing the highest serial number and using statistical analysis, they could accurately determine total production numbers and manufacturing rates. This same principle applies to modern web applications.
Competitor Analysis:
- A competitor signs up for your service and receives user ID
12345 - One month later, they create another account and receive user ID
15678 - Without obfuscation: They now know you gained ~3,333 users that month
- With CloakId: They see IDs like
A6das1andxnF9Hu- no business intelligence can be extracted
Resource Enumeration:
- Attackers often probe sequential IDs to map your system:
/api/posts/1,/api/posts/2, etc. - Without obfuscation: Reveals total post count, posting activity patterns, system growth rates
- With CloakId: Each ID appears random, preventing systematic enumeration
Remember: CloakId adds a defense-in-depth layer to make unauthorized reconnaissance significantly more difficult - it does not replace fundamental security practices.
CloakId provides a completely unintrusive solution by working transparently at the serialization boundary:
The Magic Happens at the Boundary:
- JSON Serialization: Automatic conversion from numbers → encoded strings
- JSON Deserialization: Automatic conversion from encoded strings → numbers
- Model Binding: Route parameters automatically decoded (
/users/A6das1→id: 12345)
This boundary-based approach provides clean separation between your client and server code:
On the Server Side:
- Your business logic works with native numeric types (
int,long, etc.) - No wrapper types, no special handling required
- Code remains clean and type-safe
On the Client Side:
- APIs receive and send encoded strings (
"A6das1","xnF9Hu") - No knowledge of internal numeric values
- Consistent string-based interface
Install the packages using Package Manager Console:
Install-Package CloakId
Install-Package CloakId.SqidsAlternatively, using the .NET CLI:
dotnet add package CloakId
dotnet add package CloakId.Sqidsusing CloakId;
using CloakId.Sqids;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddCloakId().WithSqids(options =>
{
options.MinLength = 6; // Configure minimum length
// options.Alphabet = "custom123456789abdefghijk"; // Custom alphabet (optional)
});
var serviceProvider = services.BuildServiceProvider();public class UserDto
{
[Cloak]
public int UserId { get; set; }
[Cloak]
public long AccountId { get; set; } // Regular properties without the attribute remain unchanged
public int RegularId { get; set; }
public string Name { get; set; }
[Cloak]
public int? OptionalId { get; set; }
}var typeInfoResolver = serviceProvider.GetRequiredService<CloakIdTypeInfoResolver>();
var jsonOptions = new JsonSerializerOptions
{
TypeInfoResolver = typeInfoResolver
};var user = new UserDto
{
UserId = 12345,
AccountId = 98765432109876,
RegularId = 999, // This remains as a number
Name = "John Doe",
OptionalId = 42
};
// Serialize - only [Cloak] properties become encoded strings
var json = JsonSerializer.Serialize(user, jsonOptions);
// Result: {"UserId":"A6das1","AccountId":"xnF9HulfM","RegularId":999,"Name":"John Doe","OptionalId":"JgaEBg"}
// Deserialize - strings decode back to original values
var deserializedUser = JsonSerializer.Deserialize<UserDto>(json, jsonOptions);
// deserializedUser.UserId == 12345
// deserializedUser.AccountId == 98765432109876
// deserializedUser.RegularId == 999 (unchanged)The [Cloak] attribute can be applied to the following numeric property types:
intandint?uintanduint?longandlong?ulongandulong?shortandshort?ushortandushort?
You can also use the codec directly for manual encoding/decoding:
var codec = serviceProvider.GetRequiredService<ICloakIdCodec>();
var originalValue = 12345;
var encoded = codec.Encode(originalValue, typeof(int)); // "A6das1"
var decoded = (int)codec.Decode(encoded, typeof(int)); // 12345CloakId includes built-in support for ASP.NET Core model binding, allowing automatic conversion of encoded route parameters:
// Enable model binding in Program.cs
builder.Services.AddCloakId().WithSqids();
builder.Services.AddControllers().AddCloakIdModelBinding();
// Use in controllers
[HttpGet("{id}")]
public IActionResult GetUser([Cloak] int id) // Automatically converts "A6das1" → 12345
{
return Ok(new { UserId = id });
}Routes like GET /api/users/A6das1 will automatically convert the encoded string to the numeric ID before reaching your controller method. See Model Binding Documentation for complete details.
CloakId provides configurable security options for model binding:
// Configure fallback behavior for enhanced security
builder.Services.AddControllers().AddCloakIdModelBinding(options =>
{
// Disable numeric fallback for better security (default: false)
// When false: only accepts encoded strings, rejects numeric IDs
// When true: accepts both encoded strings and numeric IDs (backwards compatibility)
options.AllowNumericFallback = false;
});Security Note: Setting AllowNumericFallback = false provides better security by rejecting any non-encoded values, but may break existing clients that send numeric IDs. The fallback behavior could potentially expose alphabet patterns through systematic testing.
CloakId includes built-in metrics using System.Diagnostics.Metrics for monitoring security-related behavior:
cloakid_model_binding_decoding_success_total- Successful decodingscloakid_model_binding_decoding_failure_total- Failed decodingscloakid_model_binding_numeric_fallback_total- Security-relevant: Fallback usagecloakid_model_binding_fallback_rejection_total- Rejected requests when fallback disabledcloakid_model_binding_decoding_duration_ms- Decoding performance
The numeric fallback metric is particularly important for security monitoring as it can indicate potential attempts to probe the encoding alphabet through systematic testing.
// With Sqids
services.AddCloakId().WithSqids(options =>
{
options.Alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; // Custom alphabet
options.MinLength = 8; // Minimum length of encoded strings
});
// With custom codec
services.AddCloakId().WithCustomCodec<MyCustomCodec>();
// With pre-registered Sqids
services.AddCloakId().WithRegisteredSqids();If you already have Sqids encoders registered in your DI container:
// First register your Sqids encoders
services.AddSingleton(new SqidsEncoder<int>(new SqidsOptions { /* your config */ }));
// ... register other encoders
// Then use the registered encoders
services.AddCloakId().WithRegisteredSqids();Important: You cannot call both WithSqids() and WithRegisteredSqids() on the same builder. The second call will throw an InvalidOperationException to prevent conflicting codec registrations.
You can implement your own encoding strategy by implementing ICloakIdCodec:
public class MyCustomCodec : ICloakIdCodec
{
public string Encode(object value, Type valueType) { /* ... */ }
public object Decode(string encodedValue, Type targetType) { /* ... */ }
}// Register custom codec by type
services.AddCloakId().WithCustomCodec<MyCustomCodec>();
// Register custom codec by instance
var myCodec = new MyCustomCodec();
services.AddCloakId().WithCustomCodec(myCodec);
// Register custom codec with factory
services.AddCloakId().WithCustomCodec(provider =>
{
var someService = provider.GetRequiredService<ISomeService>();
return new MyCustomCodec(someService);
});When serialized to JSON, your attributed properties will look like this:
{
"UserId": "A6das1",
"AccountId": "xnF9HulfM",
"RegularId": 999,
"Name": "John Doe",
"OptionalId": "JgaEBg"
}Instead of exposing the raw numeric values:
{
"UserId": 12345,
"AccountId": 98765432109876,
"RegularId": 999,
"Name": "John Doe",
"OptionalId": 42
}Notice how only the properties marked with [Cloak] are encoded, while RegularId remains as a number.
CloakId is designed for performance with minimal overhead. You can run comprehensive benchmarks to see the performance characteristics:
# Run all benchmarks
./run-benchmarks.ps1
# Run only encoding/decoding benchmarks
./run-benchmarks.ps1 "*Encode*"
# Run only JSON serialization benchmarks
./run-benchmarks.ps1 "*Json*"
# Run only happy path tests
./run-benchmarks.ps1 "*HappyPath*"
# Run only error handling tests
./run-benchmarks.ps1 "*SadPath*"
# Quick validation run
./run-benchmarks.ps1 "*" --dryBased on benchmarks, typical performance characteristics:
- Encoding: ~4 microseconds per int32 value
- JSON Serialization: ~40 microseconds for small models
- Memory allocation: ~21KB allocated per serialization of typical models
- Error handling: Fast exception handling for invalid data
See /benchmarks/README.md for detailed benchmark information.
- Security: Internal numeric IDs are not exposed in API responses
- Clean Code: Simple attribute-based approach, no wrapper types needed
- Selective: Choose exactly which properties to encode
- Type Safety: Full support for nullable types and type checking
- Performance: Efficient encoding/decoding with minimal overhead
- Flexibility: Easy to swap encoding strategies without changing business logic
This project is licensed under the MIT License - see the LICENSE file for details.
