Virsimo.Security
0.0.5
dotnet add package Virsimo.Security --version 0.0.5
NuGet\Install-Package Virsimo.Security -Version 0.0.5
<PackageReference Include="Virsimo.Security" Version="0.0.5" />
<PackageVersion Include="Virsimo.Security" Version="0.0.5" />
<PackageReference Include="Virsimo.Security" />
paket add Virsimo.Security --version 0.0.5
#r "nuget: Virsimo.Security, 0.0.5"
#:package Virsimo.Security@0.0.5
#addin nuget:?package=Virsimo.Security&version=0.0.5
#tool nuget:?package=Virsimo.Security&version=0.0.5
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 | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
-
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.