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
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ All notable changes to this project are documented here. This project adheres to
## 3.0.0

A major release focused on cryptographic correctness, a modern API, and broader use cases.
See the [v2 → v3 migration guide](docs/v3-target/migration-v2-to-v3.md).
See the [v2 → v3 migration guide](docs/migration-v2-to-v3.md).

### Breaking changes
- **Invalid settings now throw** `ArgumentException` from `Next()` instead of returning an error
message as the "password". Use `TryNext(out var password)` for a non-throwing path.
- **Minimum runtime is now .NET 8.** The package targets `net8.0` and `net10.0`; `netstandard2.0`
has been dropped. Consumers on .NET Framework or other older runtimes should stay on the 2.x line.

### Security / correctness fixes
- Cryptographically secure RNG (`CryptoRandomSource`) with **unbiased** integer sampling
(rejection sampling — removes modulo bias).
(via `RandomNumberGenerator.GetInt32` — removes modulo bias).
- Fixed an off-by-one in length handling and removed the GUID-based shuffle in favour of a
Fisher–Yates shuffle.
- Disposed/owned RNG lifecycle; removed dead code and the static RNG.
Expand All @@ -32,7 +34,7 @@ See the [v2 → v3 migration guide](docs/v3-target/migration-v2-to-v3.md).
`PasswordOptions.DefaultBatchCount`.

### Packaging
- Multi-targets `netstandard2.0` and `net8.0`; nullable reference types enabled.
- Multi-targets `net8.0` and `net10.0`; nullable reference types enabled.
- Single source of version truth in the csproj (removed the stale `.nuspec`).
- `PackageIcon` + `PackageReadmeFile` (clears `NU5048`), SourceLink, deterministic build, and a
`.snupkg` symbol package.
Expand All @@ -44,4 +46,4 @@ See the [v2 → v3 migration guide](docs/v3-target/migration-v2-to-v3.md).
## 2.1.0 and earlier

See the project history and the original review in
[`docs/V3_REVIEW_AND_DOCUMENTATION.md`](docs/V3_REVIEW_AND_DOCUMENTATION.md).
[`docs/archive/V3_REVIEW_AND_DOCUMENTATION.md`](docs/archive/V3_REVIEW_AND_DOCUMENTATION.md).
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
</ItemGroup>

<ItemGroup>
Expand Down
12 changes: 6 additions & 6 deletions PasswordGenerator.Tests/PasswordGenerator.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="nunit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="nunit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
</ItemGroup>

<ItemGroup>
Expand Down
48 changes: 4 additions & 44 deletions PasswordGenerator/CryptoRandomSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,18 @@
namespace PasswordGenerator
{
/// <summary>
/// <see cref="IRandomSource" /> backed by a cryptographic RNG.
/// On modern targets it uses <see cref="RandomNumberGenerator.GetInt32(int)" />; on
/// <c>netstandard2.0</c> it uses rejection sampling so the result is uniform across the whole
/// range with no modulo bias and no off-by-one.
/// <see cref="IRandomSource" /> backed by a cryptographic RNG. Uses
/// <see cref="RandomNumberGenerator.GetInt32(int)" />, which samples uniformly across the whole
/// range with no modulo bias.
/// </summary>
public sealed class CryptoRandomSource : IRandomSource, IDisposable
public sealed class CryptoRandomSource : IRandomSource
{
#if !NET8_0_OR_GREATER
private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
#endif

public int NextInt(int maxExclusive)
{
if (maxExclusive <= 0)
throw new ArgumentOutOfRangeException(nameof(maxExclusive), "maxExclusive must be positive.");

#if NET8_0_OR_GREATER
return RandomNumberGenerator.GetInt32(maxExclusive);
#else
if (maxExclusive == 1) return 0;

var range = (uint)maxExclusive;

// Largest multiple of range that is <= 2^32. Values at or above this are rejected so the
// accepted region is a whole number of buckets, giving an unbiased result mod range.
const ulong fullSpace = 1UL << 32;
var limit = fullSpace - fullSpace % range;

uint value;
do
{
value = NextUInt32();
} while (value >= limit);

return (int)(value % range);
#endif
}

#if !NET8_0_OR_GREATER
private uint NextUInt32()
{
var buffer = new byte[4];
_rng.GetBytes(buffer);
return BitConverter.ToUInt32(buffer, 0);
}
#endif

public void Dispose()
{
#if !NET8_0_OR_GREATER
_rng.Dispose();
#endif
}
}
}
6 changes: 3 additions & 3 deletions PasswordGenerator/PasswordGenerator.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>

Expand All @@ -20,7 +20,7 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>passwordgeneratorlogo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>Password,Passphrase,Generator,OWASP,NIST,Security,Random,Crypto,OTP,ApiKey,Entropy,dotnet,netstandard,DependencyInjection</PackageTags>
<PackageTags>Password,Passphrase,Generator,OWASP,NIST,Security,Random,Crypto,OTP,ApiKey,Entropy,dotnet,DependencyInjection</PackageTags>
<PackageReleaseNotes>3.0.0 is a major release: cryptographically secure RNG with unbiased sampling, exception/TryNext error handling, async APIs, dependency-injection support, presets (OWASP/NIST/OTP/API key/passphrase), custom pools, exclude-ambiguous, per-class minimums and entropy estimation. See the migration guide for upgrading from 2.x.</PackageReleaseNotes>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>

Expand All @@ -41,7 +41,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.300" PrivateAssets="All" />
</ItemGroup>

</Project>
12 changes: 5 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ Install via NuGet: ``` Install-Package PasswordGenerator ```

[Or click here to go to the package landing page](https://www.nuget.org/packages/PasswordGenerator)

It targets `netstandard2.0` and `net8.0`, so it runs on .NET Framework, .NET Core and modern .NET.
See the chart below:
It targets `net8.0` and `net10.0`, so it requires .NET 8 or later. If you need to run on .NET
Framework or other older runtimes, use the 2.x line (which targets `netstandard2.0`).

![Compatibility Chart](https://github.com/prjseal/PasswordGenerator/blob/master/compatibility.png "Compatibility Chart")

> **Upgrading from 2.x?** See the [v2 → v3 migration guide](docs/v3-target/migration-v2-to-v3.md).
> **Upgrading from 2.x?** See the [v2 → v3 migration guide](docs/migration-v2-to-v3.md).
> The v2 API still works; the one behavioural change is that invalid settings now **throw** (or use
> `TryNext`) instead of returning an error string as the "password".

Expand Down Expand Up @@ -82,7 +80,7 @@ var password = pwd.Next();
## Presets

Ready-made starting points; later fluent calls still override them. See the
[standards mapping](docs/v3-target/migration-v2-to-v3.md#6-standards-mapping-for-the-presets) for the
[standards mapping](docs/migration-v2-to-v3.md#6-standards-mapping-for-the-presets) for the
OWASP/NIST rationale.

```csharp
Expand Down Expand Up @@ -150,6 +148,6 @@ public class SignupService(IPasswordGenerator generator)

## Documentation

- [v2 → v3 migration guide](docs/v3-target/migration-v2-to-v3.md)
- [v2 → v3 migration guide](docs/migration-v2-to-v3.md)
- [Changelog](CHANGELOG.md)
- [Design & architecture docs](docs/README.md)
74 changes: 37 additions & 37 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,53 @@
# PasswordGenerator — Documentation

This folder is the working reference for the package: the **shipped v3 design** and the historical
review of the v2.1.0 code it replaced. Diagrams are written in [Mermaid](https://mermaid.js.org/) and
render directly on GitHub.
This folder is the working reference for the package as it ships today (**v3**). Diagrams are written
in [Mermaid](https://mermaid.js.org/) and render directly on GitHub.

## How the docs fit together

```mermaid
flowchart LR
subgraph Review["Review & verification"]
A[V3_REVIEW_AND_DOCUMENTATION.md<br/>full review of v2.1.0]
B[V3_VERIFICATION.md<br/>every issue re-checked vs current source]
subgraph Docs["the docs — current (v3)"]
D1[architecture.md]
D2[generation-flow.md]
D3[api-surface.md]
D4[configuration-and-di.md]
D5[migration-v2-to-v3.md]
D6[v3-local-nuget-test.md]
end
subgraph Current["current-state/ — what we have"]
C1[architecture.md]
C2[generation-flow.md]
C3[api-surface.md]
subgraph Archive["archive/ — historical"]
A1[V3_REVIEW_AND_DOCUMENTATION.md]
A2[V3_VERIFICATION.md]
A3[current-state/ v2.1.0 snapshot]
A4[before-after.md]
A5[roadmap.md]
A6[implementation-plan.md]
end
subgraph Target["v3-target/ — where we are going"]
T1[architecture.md]
T2[generation-flow.md]
T3[api-surface.md]
T4[configuration-and-di.md]
T5[before-after.md]
T6[roadmap.md]
T7[implementation-plan.md]
T8[migration-v2-to-v3.md]
end
A --> B --> Current
Current --> Target
T5 -. compares .-> Current
T6 --> T7
Docs -. superseded by .-> Archive
```

## Reading order

1. **`V3_REVIEW_AND_DOCUMENTATION.md`** — the original full review (API, bugs, packaging, gaps).
2. **`V3_VERIFICATION.md`** — each issue re-checked against the current `master` source, with verdicts.
3. **`current-state/`** — diagrammed snapshot of the v2.1.0 code (now **historical**; the issues it
documents are resolved in v3 — see the root [`CHANGELOG.md`](../CHANGELOG.md)).
4. **`v3-target/`** — the v3 design (now **shipped**), diagrammed, with a before/after, a roadmap, a
phased **`implementation-plan.md`**, and the user-facing **`migration-v2-to-v3.md`**.
1. **`architecture.md`** — type relationships, the random source, and the multi-targeting strategy.
2. **`generation-flow.md`** — how `Next()`/`Generate()` build a password, plus the async path.
3. **`api-surface.md`** — the public fluent surface, presets, and batch/async APIs.
4. **`configuration-and-di.md`** — `PasswordOptions`, settings resolution, and DI registration.
5. **`migration-v2-to-v3.md`** — the user-facing upgrade guide from 2.x.
6. **`v3-local-nuget-test.md`** — local `dotnet pack` / install verification procedure.

## Conventions

- **Current state** describes `master` @ v2.1.0, `netstandard2.0`. Code references use
`file:line` against that source.
- **v3 target** describes the design that shipped in v3.0.0 (`netstandard2.0;net8.0`, nullable
enabled). The `implementation-plan.md` records where the shipped code intentionally diverged from
the earlier proposal (e.g. the `IPasswordBuilder` split was deferred and sync methods were not
obsoleted).
- Each "target" doc ends with a **Why this is better** note tied back to a verified issue.
- These docs describe **v3.0.0** as shipped: targets `net8.0;net10.0`, nullable enabled.
- Each doc ends with a **Why this is better** note.

## Archive

`archive/` keeps the material that led to v3 but no longer describes the current state:

- **`V3_REVIEW_AND_DOCUMENTATION.md`** / **`V3_VERIFICATION.md`** — the original review and
issue-by-issue verification of the v2.1.0 code.
- **`current-state/`** — the diagrammed snapshot of the v2.1.0 (`netstandard2.0`) code that v3 replaced.
- **`roadmap.md`** / **`implementation-plan.md`** / **`before-after.md`** — the v3 planning documents.

These are point-in-time records; where they recommend or describe `netstandard2.0` support, note that
v3 dropped it (see the root [`CHANGELOG.md`](../CHANGELOG.md)).
14 changes: 7 additions & 7 deletions docs/v3-target/api-surface.md → docs/api-surface.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# v3 Target — Public API Surface
# Public API Surface

Keeps the familiar fluent feel; adds safety, presets, batch, async, and custom pools.

> **As shipped:** the fluent builder is the existing `IPassword` (the full `IPasswordBuilder`/`Build()`
> split from the early proposal was deferred). `Password` implements both `IPassword` and the new
> generation contract `IPasswordGenerator`. Passphrases return an `IPasswordGenerator`
> (`PassphraseGenerator`). See `implementation-plan.md` for the deviations.
> The fluent builder is `IPassword` (there is no separate `IPasswordBuilder`/`Build()` split).
> `Password` implements both `IPassword` and the generation contract `IPasswordGenerator`.
> Passphrases return an `IPasswordGenerator` (`PassphraseGenerator`). See
> `archive/implementation-plan.md` for how the shipped surface diverged from the early proposal.

## API map

Expand Down Expand Up @@ -69,7 +69,7 @@ fluent call still overrides them (resolution order is documented in `configurati
## Surfacing the broader purpose

The library is **not password-only**. The same surface generates OTPs, environment names, API keys,
and other identifiers — so v3 deliberately keeps the per-class `Include*` methods and adds
and other identifiers — so the library deliberately keeps the per-class `Include*` methods and adds
`WithCharacters`/`WithAllAscii` rather than forcing OWASP composition or a global 12-char minimum.

## Deprecation / migration shape
Expand All @@ -89,6 +89,6 @@ flowchart TD
> of async would be an anti-pattern and would spam every consumer with build warnings. Async exists
> for ergonomics and cancellation only.

**Why this is better:** every verified gap in `../current-state/api-surface.md` is closed
**Why this is better:** every gap noted in the v2.1.0 review (`archive/current-state/api-surface.md`) is closed
(`TryNext`/async/DI/presets/appSettings/custom pools), failures become explicit, and existing single
`.Next()` users still work unchanged, giving a gentle upgrade path.
36 changes: 18 additions & 18 deletions docs/v3-target/architecture.md → docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# v3 Target — Architecture
# Architecture

> Multi-target `netstandard2.0;net8.0`, nullable enabled. Aligns with the adjusted plan in
> `../V3_VERIFICATION.md` §3. As shipped, the existing `IPassword` remains the fluent builder (no
> Multi-targets `net8.0;net10.0`, nullable enabled. `IPassword` is the fluent builder (no
> separate `IPasswordBuilder`/`Build()`); `Password` implements both `IPassword` and the generation
> contract `IPasswordGenerator`.

Expand Down Expand Up @@ -51,7 +50,7 @@ classDiagram
+int NextInt(int maxExclusive)
}
class CryptoRandomSource {
GetInt32 on net8, rejection sampling on netstandard2.0
RandomNumberGenerator.GetInt32
}
class IEntropyEstimator {
<<interface>>
Expand All @@ -68,13 +67,13 @@ classDiagram
Password ..> PoolEntropyEstimator : EstimateEntropyBits
```

Key shifts from today:
- **`IRandomSource` abstraction** wraps the CSPRNG (unbiased `RandomNumberGenerator.GetInt32` on
`net8.0`; rejection-sampling fallback on `netstandard2.0`). No `static`, disposable-aware,
injectable. Fixes verified §5.2/§5.3/§5.6 and lets the Guid `Shuffle` be deleted (§5.5).
Key points:
- **`IRandomSource` abstraction** wraps the CSPRNG (unbiased `RandomNumberGenerator.GetInt32`). No
`static`, injectable — a deterministic `IRandomSource` can be injected in unit tests — and the
Guid-based `Shuffle` is gone in favour of Fisher–Yates.
- **`PasswordOptions`** is the DI config object, bindable from `IConfiguration`.
- **Presets** are static factory methods on `Password` that pre-fill the fluent builder.
- The `[Obsolete]` v2 wrappers are **removed** in v3 (recommended in `../V3_VERIFICATION.md` §4).
- The `[Obsolete]` v2 wrappers from earlier proposals are not present.

## Target composition (with DI)

Expand All @@ -99,19 +98,20 @@ registration is only responsible for wiring `IRandomSource` and default `Passwor

```mermaid
flowchart LR
subgraph ns["netstandard2.0 (broad reach: .NET Framework, Umbraco)"]
a["manual rejection sampling"]
end
subgraph net8["net8.0 (modern)"]
subgraph net8["net8.0"]
b["RandomNumberGenerator.GetInt32"]
end
IRandomSource --> ns
subgraph net10["net10.0"]
c["RandomNumberGenerator.GetInt32"]
end
IRandomSource --> net8
IRandomSource --> net10
```

`#if` inside `CryptoRandomSource` selects the optimal API per target while keeping one public surface.
Both targets use the same built-in `RandomNumberGenerator.GetInt32`, so `CryptoRandomSource` needs no
`#if` and exposes one uniform public surface. (`netstandard2.0`, which required a manual
rejection-sampling fallback, was dropped in v3 — see the [changelog](../CHANGELOG.md).)

**Why this is better:** removes the `static`/undisposed RNG, makes randomness unbiased and testable
(inject a deterministic `IRandomSource` in unit tests), keeps .NET Framework users supported, and
gives modern consumers the fast built-in APIs — addressing verified issues §5.2, §5.3, §5.5, §5.6 and
gaps §8 (async/DI/multi-target) at the architecture level.
(inject a deterministic `IRandomSource` in unit tests), and uses the fast, allocation-free built-in
crypto API on every supported runtime.
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PasswordGenerator — Full Package Documentation & v3 Review

> **Archived / historical.** Review of the v2.1.0 source written to plan v3, kept for reference.
> Where it recommends multi-targeting `netstandard2.0`, note that v3 dropped `netstandard2.0` and
> targets `net8.0;net10.0`. See the root [`CHANGELOG.md`](../../CHANGELOG.md).

> Purpose: a single, self-contained reference for the `PasswordGenerator` NuGet package as it
> stands today (v2.1.0). Written so it can be pasted into a Claude chat to plan v3. It covers
> what the package is, every public API, the internal implementation, confirmed bugs, design
Expand Down
Loading
Loading