This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Restore dependencies
dotnet restore
# Build (Debug or Release)
dotnet build --configuration Release
dotnet build --configuration Debug
# Run all tests
dotnet test --configuration Release --verbosity detailed
# Run a single test class
dotnet test --configuration Release --filter "ClassName=ChatSessionTests"
# Run a single test method
dotnet test --configuration Release --filter "FullyQualifiedName~ChatSessionTests.SomeTestMethod"
# Run tests with coverage
dotnet test --configuration Release --collect:"XPlat Code Coverage"
# Pack NuGet package
dotnet pack --configuration Release --output ./packagesBuild settings enforce TreatWarningsAsErrors=true and EnforceCodeStyleInBuild=true, so all analyzer warnings must be resolved before a build succeeds. CS1591 (missing XML doc comments) is suppressed.
LibEmiddle is a .NET 8 end-to-end encryption library implementing X3DH + Double Ratchet protocols via libsodium. The solution has four projects:
| Project | Role |
|---|---|
| LibEmiddle | Main library — all cryptographic logic, sessions, transport |
| LibEmiddle.Abstractions | Interface contracts only; no implementations |
| LibEmiddle.Domain | Domain models, DTOs, enums, constants — no external dependencies |
| LibEmiddle.Tests.Unit | MSTest test suite with Moq for mocking |
LibEmiddle → LibEmiddle.Abstractions → LibEmiddle.Domain
LibEmiddle.Tests.Unit references only LibEmiddle (internals are exposed via InternalsVisibleTo).
- API/ —
LibEmiddleClientis the single public entry point.LibEmiddleClientOptionscontrols all configuration. Client implementsIAsyncDisposable. - Protocol/ —
X3DHProtocol(initial key agreement) andDoubleRatchetProtocol(continuous ratcheting).ProtocolAdapterbridges them. - Crypto/ —
CryptoProviderwraps libsodium viaSodium.csP/Invoke bindings.AES.cshandles AES-256-GCM.SecureMemory.csmanages zeroing of sensitive data. - KeyManagement/ —
KeyManagerhandles key lifecycle;KeyStoragepersists keys encrypted with AES-GCM. - Sessions/ —
SessionManagermanages session lifecycle;SessionPersistenceManagerhandles bundle caching and recovery (Argon2id KDF for password-derived keys since v2.6.0). - Messaging/Chat/ —
ChatSessionhandles 1-to-1 encrypted sessions with 500-entry replay-protection FIFO buffer. - Messaging/Group/ —
GroupSessionmanages multi-party sessions with per-sender message IDs for replay protection. - Messaging/Transport/ —
MailboxManagerencrypts/decrypts;HttpMailboxTransportandInMemoryMailboxTransportimplementIMailboxTransport.SecureWebSocketClientwraps TLS WebSocket. - MultiDevice/ —
DeviceManager,DeviceLinkingService, andSyncMessageValidatorimplement device linking and revocation.
- Constants/ProtocolVersion.cs —
MAJOR_VERSION/MINOR_VERSIONconstants; updated only on major releases. - Enums/ —
KeyRotationStrategy(Aggressive/Standard/Conservative/Adaptive),SessionState,MemberRole,MessageType,SessionType. - DTO/ — Serializable snapshots for all session types (used for persistence, never for wire format directly).
LibEmiddleException— Typed exception withLibEmiddleErrorCodeenum; always throw this instead of raw exceptions in library code.
All public-facing interfaces live here: IChatSession, IGroupSession, IDoubleRatchetProtocol, IX3DHProtocol, IMailboxTransport, IKeyManager, ISessionManager, IStorageProvider, IDeviceManager, ICryptoProvider, etc. Implement these interfaces in LibEmiddle; never let implementations leak into Abstractions.
Core patterns — always apply:
- Clone session state before mutating:
DeepCloneSession()at entry of every encrypt/decrypt, return the clone, never modify the input object - Semaphore over lock:
SemaphoreSlim(1,1)for all async-safe state mutations; never uselock()in async paths - Volatile + Interlocked disposal:
private volatile bool _disposedchecked viaThrowIfDisposed()on every public entry; set withInterlocked.Exchange(ref _disposedFlag, 1) - Null-before-return for owned keys: when returning key material to a caller, set the local variable to
nullbefore thefinallyblock so the finally doesn't clear what the caller now owns - Secure-clear in finally: every method that produces intermediate key bytes wraps the work in try/finally and calls
SecureMemory.SecureClear()on all temporaries — even on the success path - Tuple returns for paired state: protocol methods return
(UpdatedSession?, ResultData?)— never mutate session in-place and return a side-channel result - Bounded eviction collections: replay-protection sets use a parallel Queue for FIFO eviction when the HashSet exceeds its cap; do the same for any unbounded accumulator
- Fire-and-forget events: raise domain events via
Task.Run(() => handler(...))so event handlers can't hold the session lock - Boolean validators, not throwing validators: key-validation helpers return
falseon bad input; only the caller that has context throws - Constant-time ops: use
CryptographicOperations.FixedTimeEquals()for key comparison,Sodium.sodium_memcmp()for raw byte comparison — never==orSequenceEqualon secrets
Anti-patterns — never do:
- Never call
Array.Clear()on key material — useSecureMemory.SecureClear()which pins and callssodium_memzero - Never use
System.Security.Cryptographyasymmetric primitives — all asymmetric ops go throughCryptoProvider→Sodium - Never add unsafe blocks outside
Core/SecureMemory.csandCore/Sodium.cs - Never swallow exceptions in catch — log and rethrow with context;
(null, null)tuple returns are the only silent failure allowed and only in protocol decrypt paths - Never share the cached key array —
GetKeyCopy()returns a fresh copy; the cache retains the original - Never use
Task.FromResultto wrap genuinely async work — only for operations that are provably synchronous - Never skip timestamp validation on incoming messages — reject anything negative or more than 1 hour in the future before touching crypto
- Cryptography: All crypto operations go through
CryptoProvider→SodiumP/Invoke. Never useSystem.Security.Cryptographydirectly for asymmetric operations; libsodium is the source of truth. - Unsafe code: Enabled in
LibEmiddle.csprojsolely for libsodium P/Invoke andSecureMemoryzeroing. Do not add unsafe blocks elsewhere. - Nullable: Nullable reference types are enabled project-wide (
Nullable=enable). All public APIs must be null-annotated. - Async: All I/O and session operations are async/await. Use
IAsyncDisposablefor resources that hold crypto state. - Events: State changes (message received, member added, key rotated, session state changed) are surfaced via typed
EventArgssubclasses in Domain. - Version bumps: Update
Directory.Build.props(VersionPrefix) and for major releases alsoProtocolVersion.cs. - Native binaries: libsodium DLLs live in
runtimes/win-x64/native/andruntimes/win-x86/native/. Linux/macOS builds rely on the system-installed libsodium.
dotnet.ymlruns matrix builds (Debug + Release) on push/PR tomain,legacy-1.0, andexperimental. Tests must pass in both configurations.- Version tags (
v*) trigger a GitHub Release with attached.nupkgfiles and auto-extracted changelog notes. release.ymlis a manual workflow for triggering versioned releases; it validates branch/version compatibility before committing the version bump and creating the tag.- Branch → version mapping:
main= v2.x.x,legacy-1.0= v1.x.x,experimental= v3.x.x-alpha.