Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ CAS .NET SDK is a managed cryptographic toolkit published as the [`cas-dotnet-sd
- `cas-dotnet-sdk/` — the SDK class library (NuGet package source). Multi-targets `net6.0`–`net10.0`.
- `cas-dotnet-sdk-tests/` — xUnit test project, references the SDK directly.
- `cas-core-lib/` — **git submodule** containing the Rust source compiled to `cas_core_lib.dll` (Windows) / `libcas_core_lib.so` (Linux) / `libcas_core_lib.dylib` (macOS). Clone with `--recursive` or run `git submodule update --init --recursive`.
- `docs/EXAMPLES.md` — usage samples.
- `docs/EXAMPLES.md` — user-facing usage samples. Must be kept current with the public API (see "Keeping examples in sync").

## Build & Test

Expand Down Expand Up @@ -39,36 +39,62 @@ The same commands work for local development on macOS (Apple Silicon): a Rust to

Note: CI (`.github/workflows/pr-tests-*.yml`) builds `cargo build --release` separately, then copies the native lib into the test `bin/Release/<tfm>` directory before running `dotnet test`. If you build the Rust lib by hand and the MSBuild target is skipped, the native artifact must end up next to the test assembly the same way.

## Architecture: the three-layer wrapper pattern
## Architecture

Every cryptographic capability is grouped into a top-level folder by category (`Hashers`, `Symmetric`, `Asymmetric`, `Signatures`, `PasswordHashers`, `KeyExchange`, `Hybrid`, `Compression`, `Sponges`, `PQC`). Within each category folder, the same three-part structure repeats — understand it once and it applies everywhere:
Every cryptographic capability is grouped into a top-level folder by category (`Hashers`, `Symmetric`, `Asymmetric`, `Signatures`, `PasswordHashers`, `KeyExchange`, `Hybrid`, `Compression`, `Sponges`, `PQC`). Each category has the same structure:

1. **Public wrapper** (e.g. `Hashers/SHAWrapper.cs`) — the user-facing class. Inherits `BaseWrapper` and implements a category interface (e.g. `IHasherBase`). It contains the real logic: choose the platform DLL import, call it, copy the returned bytes out of the native pointer with `Marshal.Copy`, then free the native memory.
1. **Public wrapper** (e.g. `Hashers/SHAWrapper.cs`) — the user-facing `unsafe` class, implementing a category interface where one exists (e.g. `IHasherBase`, `IPasswordHasherBase`). Each method pins its inputs with `fixed`, calls the generated binding on `NativeMethods`, checks the result's `error_code`, then copies the output into managed memory and frees the native allocation.

2. **Platform P/Invoke wrappers** (`<Category>/Windows/*.cs` and `<Category>/Linux/*.cs`) — `internal static` classes containing only `[DllImport]` declarations. Windows imports target `cas_core_lib.dll`, Linux imports target `libcas_core_lib.so`. The two files are otherwise identical signatures. The public wrapper dispatches between them with `this._platform == OSPlatform.Linux ? LinuxWrapper.fn(...) : WindowsWrapper.fn(...)`.
2. **Generated bindings** (`generated/NativeMethods.g.cs`) — `[DllImport]` declarations and C ABI result structs (e.g. `CasStringResult`, `CasVerifyResult`) emitted by csbindgen from `cas-core-lib/build.rs` during the `BuildRust` target. Never edit this file by hand.

3. **Types** (`<Category>/Types/*.cs`) — `internal struct`s mirroring the C ABI structs returned by the Rust FFI (e.g. `SHAHashByteResult { IntPtr result_bytes_ptr; int length; }`), plus category interfaces and enums.
3. **Types** (`<Category>/Types/*.cs`) — public result classes returned to callers (e.g. `Ed25519KeyPairResult`, `HpkeEncryptResult`), plus category enums.

### Cross-cutting helpers (`Helpers/`)

- `BaseWrapper` — base class for all public wrappers; resolves the current OS into `_platform` (`OSPlatform`) at construction via `OperatingSystemDeterminator`. Throws on any OS that is not Windows or Linux.
- `FreeMemoryHelper` — frees native heap memory returned by the Rust layer. **Any FFI call that returns a pointer-bearing struct must have its pointer freed via `FreeMemoryHelper` after the bytes are marshaled out**, or memory leaks. This is the single most important invariant when adding new wrappers.
- `CasErrorHandler.ThrowIfError` — turns a non-zero `error_code` into a `CasException` (carrying `CasErrorCode` and the raw code). **Call it before reading any pointer from a result**: the native layer leaves pointers null on failure.
- `NativeByteBuffer.CopyAndFree` / `NativeString.ReadAndFree` — copy a native buffer or C string into managed memory and free it through the FFI. **Every pointer returned by the native layer must go through one of these**, or memory leaks. This is the single most important invariant when adding new wrappers.
- `NativeString.ToCString` — encodes a managed string as a null-terminated UTF-8 buffer for pinning.
- `NativePin.Of` — substitutes a non-empty sentinel for zero-length arrays so `fixed` yields a non-null pointer.

### Factories

Categories with interchangeable algorithms expose a `*Factory` (e.g. `HasherFactory.Get(IHasherType.SHA)`) returning the category interface. When adding a new algorithm to such a category, wire it into both the enum and the factory switch.
Categories with interchangeable algorithms expose a `*Factory` (e.g. `HasherFactory.Get(IHasherType.SHA)`, `PasswordHasherFactory.Get(...)`) returning the category interface. When adding a new algorithm to such a category, wire it into both the enum and the factory switch.

### Tunable cost

The password hashers expose `HashPasswordWithParameters` alongside the default `HashPassword`: Argon2 (`memoryCost` in MiB — cas-lib multiplies it by 1024, `iterations`, `parallelism`), SCrypt (`cpuCost` as log2(N), `blockSize`, `parallelism`) and BCrypt (`cost`, max 31).

## Adding a new algorithm

1. Add the Rust FFI function in `cas-core-lib` (separate submodule/repo) and rebuild it.
2. Add matching `[DllImport]` declarations in **both** the `Windows` and `Linux` P/Invoke files of the relevant category.
3. Add any new return struct to `Types/`.
4. Implement the public method on the wrapper: platform-dispatch the call, marshal out the result, free native memory.
5. If the category has a factory/enum, register the new type there.
6. Add an xUnit test. NIST test vectors live as `.rsp` files under the test project's `AES/Data` and `SHA/Data` folders (copied to output via `Content` items in the test `.csproj`).
1. Add the Rust FFI function in `cas-core-lib` (separate submodule/repo), returning a result struct with an `error_code`, and rebuild — csbindgen regenerates `generated/NativeMethods.g.cs`.
2. Implement the public method on the wrapper: pin inputs with `fixed`, call `NativeMethods`, `CasErrorHandler.ThrowIfError`, then marshal out with `NativeByteBuffer` / `NativeString`.
3. Add any new public result class to `Types/`.
4. If the category has a factory/enum, register the new type there.
5. Add an xUnit test. NIST test vectors live as `.rsp` files under the test project's `AES/Data` and `SHA/Data` folders (copied to output via `Content` items in the test `.csproj`).
6. Add a usage example to `docs/EXAMPLES.md` (see below).

## Keeping examples in sync

`docs/EXAMPLES.md` is the user-facing reference and must cover **every public method** in `cas-dotnet-sdk/`. **Whenever you add, rename, remove, or change the signature of a public method or result type, update `docs/EXAMPLES.md` in the same change, without being asked** — this is part of the task, not a follow-up. A change to the public API is not done until its example is.

Before finishing any task that touches a public wrapper, run this coverage check from the repo root (Git Bash). It must print nothing; each `MISSING` line is a method without an example:

```bash
git grep -hoE "^\s+public [A-Za-z0-9<>\[\]]+ [A-Z][A-Za-z0-9]*\(" -- 'cas-dotnet-sdk/*.cs' ':!cas-dotnet-sdk/generated' ':!cas-dotnet-sdk/Helpers' \
| sed -E 's/.* ([A-Za-z0-9]+)\($/\1/' | sort -u \
| while read m; do grep -q "\.$m(" docs/EXAMPLES.md || echo "MISSING $m"; done
```

- **New method or algorithm:** add an example under its category heading (create the heading if needed), with the `using` lines and a round trip where one applies (encrypt + decrypt, sign + verify, hash + verify).
- **Tunable parameters / cost:** show the parameterized overload next to the default one and list each parameter with its unit, default, and limits (as the Password Hashers section does).
- **Argument order:** copy it from the wrapper's signature and the xUnit test, and run the snippet against the built native library when you can. Hash `Verify` methods differ: `SHAWrapper.Verify*(data, hash)` but `Blake2Wrapper.Verify*(hash, data)`.
- **Renamed or removed APIs:** fix or delete every example that calls them; grep `docs/EXAMPLES.md` for the old name.
- **New result types:** use the real property names from `Types/` in the example.
- Also give the public method XML doc comments (`<summary>`, plus `<param>` for any tuning parameter).
- Keep the doc example-only: no benchmark tables or performance claims.

## Cross-platform constraint

Supported targets: **Windows x64**, **Linux x64**, and **macOS Apple Silicon** (`osx-arm64`). Intel macs (`osx-x64`) are intentionally not supported — the Intel runners were too slow for this project's CI. Since the csbindgen migration (#191), P/Invoke goes through the generated `generated/NativeMethods.g.cs` using the bare library name `cas_core_lib`; the .NET runtime resolves it per-OS to `cas_core_lib.dll` / `libcas_core_lib.so` / `libcas_core_lib.dylib`, so there are no per-platform `[DllImport]` files to keep in sync. The native lib is built (`cargo build --release`), staged into `artifacts/native/<rid>/`, and packed into `runtimes/<rid>/native` for each RID. Tests run against .NET 6–10 on all three OSes in CI on every PR: `pr-tests-windows` and `pr-tests-linux` fan out one job per TFM, while `pr-tests-macos-arm64` (`macos-latest`, Apple Silicon) runs all five target frameworks in a single `dotnet test` invocation (no `-f`). The `OperatingSystemDeterminator` resolves Windows, Linux, and OSX and throws on anything else.
Supported targets: **Windows x64**, **Linux x64**, and **macOS Apple Silicon** (`osx-arm64`). Intel macs (`osx-x64`) are intentionally not supported — the Intel runners were too slow for this project's CI. Since the csbindgen migration (#191), P/Invoke goes through the generated `generated/NativeMethods.g.cs` using the bare library name `cas_core_lib`; the .NET runtime resolves it per-OS to `cas_core_lib.dll` / `libcas_core_lib.so` / `libcas_core_lib.dylib`, so there are no per-platform `[DllImport]` files to keep in sync. The native lib is built (`cargo build --release`), staged into `artifacts/native/<rid>/`, and packed into `runtimes/<rid>/native` for each RID. Tests run against .NET 6–10 on all three OSes in CI on every PR: `pr-tests-windows` and `pr-tests-linux` fan out one job per TFM, while `pr-tests-macos-arm64` (`macos-latest`, Apple Silicon) runs all five target frameworks in a single `dotnet test` invocation (no `-f`).

Note: this is desktop **macOS / Darwin**, not iOS. Real iOS (`aarch64-apple-ios`) would need a cross-compiled native lib and a simulator/device to run tests, neither of which the current CI provides.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CAS .NET SDK is a comprehensive cryptographic toolkit for .NET, designed to prov

## Features
- Modern cryptographic primitives: digital signatures (RSA, Ed25519), hashing, and more
- Password hashing (Argon2, SCrypt, BCrypt) with configurable cost parameters
- Seamless integration with [cas-lib](https://github.com/Cryptographic-API-Services/cas-lib) Rust FFI layer for optimal performance
- Unified interface: no need to manage multiple cryptography packages or surf disparate documentation
- Built on trusted, open-source cryptography libraries
Expand Down
4 changes: 4 additions & 0 deletions cas-dotnet-sdk/PasswordHashers/Argon2Wrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public Argon2Wrapper()
/// Hashes the specified password using the Argon2 algorithm with the provided memory cost, iteration count, and
/// parallelism parameters.
/// </summary>
/// <param name="memoryCost">Memory to use, in MiB.</param>
/// <param name="iterations">Number of passes over memory (time cost).</param>
/// <param name="parallelism">Number of threads (lanes).</param>
/// <param name="passToHash">The password to hash.</param>
public string HashPasswordWithParameters(int memoryCost, int iterations, int parallelism, string passToHash)
{
if (string.IsNullOrEmpty(passToHash))
Expand Down
2 changes: 2 additions & 0 deletions cas-dotnet-sdk/PasswordHashers/BcryptWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public string HashPassword(string passwordToHash)
/// Hashes a password using the BCrypt algorithm with specified parameters.
/// Max cost is 31.
/// </summary>
/// <param name="passToHash">The password to hash.</param>
/// <param name="cost">Log2 work factor; each +1 doubles the hashing time.</param>
public string HashPasswordWithParameters(string passToHash, uint cost = 12)
{
fixed (byte* passPtr = NativeString.ToCString(passToHash))
Expand Down
7 changes: 7 additions & 0 deletions cas-dotnet-sdk/PasswordHashers/SCryptWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public string HashPassword(string passToHash)
}
}

/// <summary>
/// Hashes a password using the SCrypt algorithm with specified parameters.
/// </summary>
/// <param name="passToHash">The password to hash.</param>
/// <param name="cpuCost">CPU/memory cost as log2(N); 17 means N = 131072.</param>
/// <param name="blockSize">Block size (r).</param>
/// <param name="parallelism">Parallelization (p).</param>
public string HashPasswordWithParameters(string passToHash, int cpuCost = 17, int blockSize = 8, int parallelism = 1)
{
if (string.IsNullOrEmpty(passToHash))
Expand Down
3 changes: 2 additions & 1 deletion cas-dotnet-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CAS .NET SDK is a comprehensive cryptographic toolkit for .NET, designed to prov

## Features
- Modern cryptographic primitives: digital signatures (RSA, Ed25519), hashing, and more
- Password hashing (Argon2, SCrypt, BCrypt) with configurable cost parameters
- Seamless integration with [cas-lib](https://github.com/Cryptographic-API-Services/cas-lib) Rust FFI layer for optimal performance
- Unified interface: no need to manage multiple cryptography packages or surf disparate documentation
- Built on trusted, open-source cryptography libraries
Expand All @@ -23,7 +24,7 @@ We build on the work of leading cryptography projects. For in-depth algorithm de
- [Rust Crypto](https://github.com/RustCrypto)

## Usage Examples
See practical usage and code samples in our [Examples](./docs/EXAMPLES.md).
See practical usage and code samples in our [Examples](https://github.com/Cryptographic-API-Services/cas-dotnet-sdk/blob/main/docs/EXAMPLES.md).

## Supported Frameworks / Operating Systems
We aim to provide cross-compatibility wherever possible. Test cases are run on .NET 6-10 for Windows, Linux (Ubuntu), and macOS on each pull request and release to NuGet through GitHub Actions.
Expand Down
2 changes: 1 addition & 1 deletion cas-dotnet-sdk/cas-dotnet-sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Title>cas-dotnet-sdk</Title>
<PackageIcon>icon.jpeg</PackageIcon>
<Version>1.9.77</Version>
<Version>1.9.78</Version>
<Authors>Mike Mulchrone</Authors>
<Description>A Nuget package that provides a implementation of the RustCrypto suite of cryptographic algorithms.</Description>
<RepositoryUrl>https://github.com/Cryptographic-API-Services/cas-dotnet-sdk</RepositoryUrl>
Expand Down
Loading
Loading