Virsimo.Security 0.0.5

dotnet add package Virsimo.Security --version 0.0.5
                    
NuGet\Install-Package Virsimo.Security -Version 0.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Virsimo.Security" Version="0.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Virsimo.Security" Version="0.0.5" />
                    
Directory.Packages.props
<PackageReference Include="Virsimo.Security" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Virsimo.Security --version 0.0.5
                    
#r "nuget: Virsimo.Security, 0.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Virsimo.Security@0.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Virsimo.Security&version=0.0.5
                    
Install as a Cake Addin
#tool nuget:?package=Virsimo.Security&version=0.0.5
                    
Install as a Cake Tool

Virsimo.Security

Curve25519 (X25519) key agreement, AES-256-CBC and SHA-256 helpers for .NET.

The scalar multiplication is a managed port of the classic curve25519 reference implementation and is verified against the RFC 7748 known-answer tests. On top of it sits a small convenience layer that keeps every key and ciphertext as a base64 string.

dotnet add package Virsimo.Security

Targets net11.0. Version 0.0.4 targeted netstandard2.1; 0.0.5 is wire-compatible with it (see Compatibility).


Quick start

Key agreement plus encryption

using Virsimo.Security;

string alicePrivate = Curve25519AES.CreateRandomPrivateKey();
string alicePublic  = Curve25519AES.GetPublicKey(alicePrivate);

string bobPrivate = Curve25519AES.CreateRandomPrivateKey();
string bobPublic  = Curve25519AES.GetPublicKey(bobPrivate);

// Alice encrypts for Bob.
string cipherText = Curve25519AES.Encrypt("hello", alicePrivate, bobPublic);

// Bob decrypts with his private key and Alice's public key.
string plainText = Curve25519AES.Decrypt(cipherText, bobPrivate, alicePublic);

For binary payloads use Curve25519AES.DecryptBytes, which returns the plaintext unchanged. Decrypt decodes as UTF-8 and will replace invalid sequences with U+FFFD.

Raw X25519

byte[] privateKey = Curve25519.CreateRandomPrivateKey();   // 32 bytes, clamped
byte[] publicKey  = Curve25519.GetPublicKey(privateKey);
byte[] shared     = Curve25519.GetSharedSecret(privateKey, peerPublicKey);

GetPublicKey and GetSharedSecret use the private key exactly as given — they do not clamp it. Use CreateRandomPrivateKey or run a stored key through ClampPrivateKey first, otherwise results will not match other X25519 implementations.

AES

byte[] key = RandomNumberGenerator.GetBytes(32);

string cipherText = AES.Encrypt(Encoding.UTF8.GetBytes("hello"), key);
byte[] plainText  = AES.DecryptBytes(Convert.FromBase64String(cipherText), key);

The wire format is base64(IV ‖ AES-256-CBC-PKCS7 ciphertext) with a 16-byte random IV.

SHA-256

string hex    = SHA.ComputeSHA256Hash("abc");            // lowercase hex
byte[] digest = SHA.ComputeSHA256Hash(someBytes);
int written   = SHA.ComputeSHA256Hash(source, destination);  // span overload, no allocation

Security considerations

Read this before using the library for anything that matters. These are properties of the design, not bugs that a patch release can quietly fix, and they are covered by tests that assert the current behaviour.

The AES layer is unauthenticated. CBC with no MAC is malleable: an attacker who can modify a ciphertext can make predictable changes to the plaintext, and decryption will still succeed. Failed padding raises a distinguishable CryptographicException, which is a padding oracle. If you need integrity — and you almost always do — use System.Security.Cryptography.AesGcm, or wrap these ciphertexts in an HMAC you verify before decrypting.

Passphrases are not stretched. AES.GetPaddedBytesWithZeros takes the UTF-8 bytes of a string and zero-pads them to 32 bytes. There is no salt, no iteration count and no memory hardness, so "hunter2" becomes a trivially guessable key, and anything past 32 bytes is silently dropped. The string-keyed Encrypt/Decrypt overloads go through it. For passphrase-derived keys use PBKDF2 (Rfc2898DeriveBytes) or Argon2 and pass the resulting 32 bytes to the byte[] overloads.

Small-order peer public keys are not rejected. A peer who sends the all-zero point forces an all-zero shared secret. If peer keys arrive over the network, reject a shared secret that is all zeros before using it.

Key derivation has no context binding. Curve25519AES uses SHA-256(shared secret) directly rather than HKDF, so there is no salt, no protocol binding, and both directions of a conversation use the same key. Static key pairs also mean no forward secrecy.

The curve implementation is not side-channel hardened. The Montgomery ladder selects points by indexing an array with a secret bit, and packing branches on secret data. Do not use it where an attacker can measure timing or cache behaviour of the process.

GetSigningKey is not a standard signature scheme. It belongs to the legacy curve25519 signature construction, has no known-answer tests here, and should not be used. Use Ed25519.


Compatibility

0.0.5 is a drop-in replacement for 0.0.4 on the wire. The test suite loads the published 0.0.4 assembly into a separate AssemblyLoadContext and checks both directions on every public path: ciphertexts, public keys, shared secrets, clamping and hashes all match, and text encrypted by one version decrypts on the other.

Behaviour that intentionally changed:

Area 0.0.4 0.0.5
Curve25519.GetSigningKey always threw IndexOutOfRangeException works (loop counter bug fixed)
Binary payloads Decrypt returned a string, corrupting non-UTF-8 bytes DecryptBytes returns them intact
Truncated ciphertext ArgumentOutOfRangeException from an internal slice, or an empty string rejected up front
AesCryptoServiceProvider used throughout replaced with Aes.Create()

The public API is otherwise unchanged; DecryptBytes, the SHA span overload and the IvSize / KeySize constants are additive.


Performance

dotnet run --project bench/Virsimo.Security.Bench -c Release, .NET 11, best of three runs. Lower is better; the change column is 0.0.5 against 0.0.4.

Operation 0.0.4 0.0.5 Change
Curve25519.GetPublicKey 135.8 us 131.0 us -3.5%
Curve25519.GetSharedSecret 142.6 us 136.3 us -4.4%
AES.Encrypt (15 B) 2.88 us 2.39 us -17.2%
AES.Decrypt (15 B) 4.11 us 1.53 us -62.8%
AES.Encrypt (64 KiB) 132.9 us 114.1 us -14.1%
AES.Decrypt (64 KiB text) 100.5 us 59.5 us -40.8%
AES.DecryptBytes (64 KiB binary) 547.2 us 13.0 us -97.6%
SHA.ComputeSHA256Hash (string) 1.21 us 0.34 us -72.3%
SHA.ComputeSHA256Hash (bytes) 30.8 us 30.5 us -1.1%

The AES and SHA gains come from one-shot span APIs replacing CryptoStream, StreamReader and LINQ-based hex formatting. The scalar multiplication is unchanged arithmetic, so it moves only with the runtime. The last row is the largest gain because 0.0.4 had no way to get binary plaintext back: callers paid for a UTF-8 decode that also corrupted the data.


Building

dotnet build -c Release
dotnet test  -c Release
dotnet run --project bench/Virsimo.Security.Bench -c Release

legacy/Virsimo.Security.0.0.4.dll is the published 0.0.4 assembly, committed so the compatibility and benchmark comparisons are reproducible without a network fetch.


License

Proprietary. See LICENSE.txt. Copyright (c) 2026 Fredrik Karlsson.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net11.0

    • 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
0.0.5 155 8/4/2026
0.0.4 374 4/7/2023
0.0.3 339 4/7/2023
0.0.2 333 4/7/2023
0.0.1 331 4/7/2023