Oğuzhan KARAGÜZEL

OĞUZHAN KARAGÜZEL

Software Developer

Open source · NuGet package · MIT licence

SpectraUtils

A library for the small things every .NET project built for Turkey ends up writing again: Turkish ID number, tax number and IBAN validation, name formatting that follows Turkish rules, PBKDF2 password hashing and secure code generation.

Elevare, the CMS this site runs on, uses SpectraUtils for its user names and name formatting.

Program.cs
$ dotnet add package SpectraUtils

using SpectraUtils.Validation;

bool tckn = TurkishIdentityNumberValidator.IsValid("10000000146");  // true
bool vkn  = TurkishTaxNumberValidator.IsValid("1739750504");     // true
bool iban = IbanValidator.IsValid("TR33 0006 1005 1978 6457 8413 26", expectedCountryCode: "TR");

string name = names.FullNameCorrection("oğuzhan", "karagüzel");  // "Oğuzhan KARAGÜZEL"

01 · Story

Why I wrote it

Almost every application built for the Turkish market runs into the same small questions. Is the national ID number the user typed actually valid? Does the tax number's check digit add up? Is the IBAN missing a digit? Names are a problem of their own: "ismail" in capitals should read "İSMAİL", but on a server set to English, .NET gives you "ISMAIL".

I got tired of writing this code from scratch for every new project, or copying it over from an old one. In 2023 I put it all in one package and published it on NuGet. Since then it has grown with use: fixed when something broke, extended when a new need came up. Version 3.0 is where I went through all of it again.

02 · Packages

Two packages, take what you need

The core package does not depend on ASP.NET Core, so it works just as well in a console app or a background worker. File upload validation lives in a separate package.

Core

SpectraUtils

Validators and validation attributes, name formatting, password hashing, token, one-time code and password generation, LINQ and string extensions. Its only outside dependencies are Microsoft's DI and Options abstractions.

dotnet add package SpectraUtils
ASP.NET Core add-on

SpectraUtils.AspNetCore

The [AllowedExtensions] attribute for file uploads. It checks the extension and size of an IFormFile or IFormFileCollection. You only need it if you accept files through forms.

dotnet add package SpectraUtils.AspNetCore

03 · Contents

What's inside

Small pieces, each solving one problem. Use the ones you need and ignore the rest.

01

Turkish ID number

Validates both check digits. [TCIDValidation] treats an empty value as invalid too, so it makes the field required on its own.

02

Tax number

Computes the check digit of the 10-digit Turkish tax number with the official algorithm. Spaces are ignored.

03

IBAN

ISO 13616 and MOD-97, plus a length check per country: an IBAN starting with TR has to be 26 characters long.

04

Minimum age

[MinimumAge(18)] works out the age from a date of birth. It takes DateOnly, DateTime and DateTimeOffset, and reads today's date from TimeProvider, so a test can pin it to any day.

05

Turkish names

In comes " aYşe nur ", out goes "Ayşe Nur". Surnames are capitalised, and i/İ and ı/I never get mixed up. It can also turn a name into a user name.

06

Password storage

PBKDF2-HMAC-SHA256 with 600,000 iterations, the values OWASP recommends today. Every hash carries its own settings, so changing them never breaks the hashes you already have.

07

Secure generators

Password reset tokens, codes sent by SMS, random passwords. All of them come from RandomNumberGenerator, without modulo bias.

08

File uploads

Allowed extensions and a size limit. When several files are sent, each one is checked, and the limit reads as KB or MB in the error message.

09

Extensions

WhereIf for conditional filters, ReverseSafe for reversing text without breaking emoji, Tap and ForEach: small helpers you reach for every day.

04 · Usage

A few examples

Installing takes one line. The rest is the .NET you already know: dependency injection and DataAnnotations.

Register the services

builder.Services.AddSpectraUtils();

// To change the settings:
builder.Services.AddSpectraUtils(options =>
{
    options.Pbkdf2.Iterations = 700_000;
});

All services are stateless and registered as singletons. They are added with TryAdd, so your own registrations take precedence. Not using DI? Every class can be created with new.

Validate a model

public class RegisterRequest
{
    [TCIDValidation]
    public string? IdentityNumber { get; set; }

    [TurkishTaxNumberValidation]
    public string? TaxNumber { get; set; }

    [IbanValidation("TR")]
    public string? Iban { get; set; }

    [MinimumAge(18, ErrorMessage = "You must be at least {1} years old.")]
    public DateOnly? BirthDate { get; set; }
}

In error messages, {0} becomes the field's name and {1} the age limit. ErrorMessageResourceType works too, if your messages live in resource files.

Store passwords

string hash = hasher.Hash("Password123!");
// PBKDF2$SHA256$600000$<salt>$<key>

if (hasher.Verify(input, user.PasswordHash))
{
    if (hasher.NeedsRehash(user.PasswordHash))
        user.PasswordHash = hasher.Hash(input);
}

NeedsRehash upgrades a hash made with older settings the moment the user signs in. Nobody has to change their password.

Format names

names.NameCorrection("  aYşe   nur ");             // "Ayşe Nur"
names.NameCorrection("ismail");                    // "İsmail"
names.SurnameCorrection("yılmaz");                 // "YILMAZ"
names.FullNameCorrection("oğuzhan", "karagüzel");  // "Oğuzhan KARAGÜZEL"
names.StandardizeCharacters("Çağdaş Işık");        // "Cagdas Isik"
names.CreateUserName("Ayşe Nur");                  // "aysenur0427", for example

Turkish rules apply by default. For another culture, change NameCulture in the options.

Generate tokens, codes and passwords

string token    = tokenGenerator.GenerateToken();          // 32 random bytes, safe in a URL
string code     = otpGenerator.GenerateNumericCode();      // "048213"
string password = passwordHelper.CreateStandardPassword(); // 12 characters, at least one of each kind

Treat one-time codes as short-lived secrets: expire them within minutes and limit the attempts.

05 · Version 3.0

What got fixed

3.0 is the release where I reviewed the library from top to bottom. Some of the bugs I found were embarrassing. I hid none of them; they are all in the release notes.

  1. About 60% of valid tax numbers were rejected

    In the official algorithm, an intermediate result that is not zero and divides by 9 counts as 9. Version 2.x counted it as 0. Even the sample number the old version shipped with was actually invalid.

  2. A very long input could crash the process

    The IBAN and tax number validators allocated stack memory in proportion to the input. A multi-megabyte string sent to an API ended the process with a stack overflow. Input length is now capped before anything is allocated.

  3. Long one-time codes were weak

    Codes of 10 to 18 digits were drawn from the int.MaxValue range, and none of them started with a digit from 3 to 9. Every digit is now picked independently and with equal probability.

  4. Turkish letters disappeared

    In globalization-invariant mode, common in small Docker images, the name "Ayşe" became the user name "aye". The result is now the same everywhere.

  5. The server's file system could be probed

    The file upload attribute called File.Exists with a string the user sent, which let anyone test which files existed on the server. That call is gone.

  6. Two first names became one

    Formatting "ahmet can" used to give "Ahmetcan". Words are now kept, and the result is "Ahmet Can". The same goes for surnames.

3.0 also targets .NET 8 and .NET 10 and tidies up the namespaces. If you are on 2.x, the upgrade steps are in the release notes. Stored password hashes need nothing: hashes made by 2.x still verify.

06 · Principles

How it behaves

The worst thing a helper library can do is surprise you. These rules were there from the start.

Validators don't throw

For null, empty, malformed or oversized input they simply return false. No try/catch just to check a field.

Comparisons run in constant time

Password verification does not reveal, through timing, how much of a wrong password was right. A malformed hash gives false, not an exception.

Settings don't break old data

Change the iteration count, salt size or algorithm and old hashes keep working. Tampered hashes with absurd iteration counts are rejected.

07 · Setup

Get started in three steps

  1. Add the package

    dotnet add package SpectraUtils
  2. Register the services

    using SpectraUtils.Extensions;
    
    builder.Services.AddSpectraUtils();
  3. Use it wherever you need it

    public class SignUpService(IPasswordHasher hasher, INameEdit names)
    {
        // ...
    }

    Interfaces live in SpectraUtils.Abstractions, attributes in SpectraUtils.Attributes.ValidationAttributes.

The source is on GitHub

Found a bug, or missing a validator? Open an issue on the repository. Please report security issues privately, through GitHub's security advisories, rather than in a public issue.