diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index 82263c6..7f25888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added exact weighted sampling without replacement and versioned executable seeded recipes. +- Added a bounded, domain-separated WebCrypto commit–reveal ceremony and verifier. + ## 0.1.1 - 2026-09-01 - Removed the external randomness-service client and its network permission; Random Tools is now entirely local-only. diff --git a/README.md b/README.md index c80678b..cd7b063 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,19 @@ Generate secure or reproducible random values locally in the browser. Random Tools is a standalone local-first application in the [add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal). Inputs are processed in the browser and are not uploaded. -## Version 0.1 scope +## Current scope - Unbiased local WebCrypto integers and strings plus normal-distribution samples - Explicitly non-cryptographic, seeded deterministic generation with reproduction metadata - Dice expressions, sampling without replacement, Fisher–Yates shuffling and passphrases +- Exact weighted sampling without replacement from bounded quoted CSV input +- Versioned executable seeded recipes for integers, strings, equal-weight samples and weighted samples +- WebCrypto commit–reveal ceremonies with domain-separated commitments, 256-bit private nonces, canonical participant ordering and fail-closed verification - UUIDv4, UUIDv7 and random ULID generation - Local draws for coin flips, shuffled card deals and unique integer sequences - Random calendar dates with weekday and uniqueness controls, exact decimal fractions, and equal-area spherical coordinates -All functionality runs locally. Secure generation never falls back to the seeded source, and deterministic results are reproducible but not suitable for secrets. Recipes for custom passphrases identify the normalized list by count and SHA-256 but do not embed it, so reproduction requires the same custom input. The browser CSPRNG is not physical entropy, and no operation is presented as certified for regulated drawings or gambling. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +All functionality runs locally. Secure generation never falls back to the seeded source, and deterministic results/recipes are reproducible but not suitable for secrets. Recipes can embed list values, so review them before sharing. Recipes for custom passphrases identify the normalized list by count and SHA-256 but do not embed it, so reproduction requires the same custom input. A commit–reveal result proves only that the supplied reveals match the supplied commitments; participants must publish every commitment before any reveal through a channel of their choice. The browser CSPRNG is not physical entropy, and no operation is presented as certified for regulated drawings or gambling. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). ## Development @@ -27,7 +30,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/rand-tools-0.1.1.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/rand-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/SOURCE.md b/SOURCE.md index 43795c7..31da0fb 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Random Tools 0.1.1 is available at: +The corresponding source for Random Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/rand-tools/src/tag/v0.1.1 +https://git.add-ideas.de/lotobo/rand-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 36eb28a..a657618 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -4,9 +4,9 @@ Random Tools 0.1.1 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 296606b..53ee53c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,4 +4,8 @@ Random Tools is a static React/Vite application wrapped in the shared Toolbox sh `random/generators.ts` builds bounded numbers, strings, identifiers, dice, samples, shuffles, passphrases and normal samples on that interface. `random/draws.ts` adds local binary coin flips, standard playing-card deals, shuffled inclusive integer sequences, Gregorian calendar-date sampling, exact decimal-digit fractions and equal-area points on a spherical surface model. Date sampling can filter weekdays and use a partial Fisher–Yates mapping for selection without replacement; it does not allocate every date in a large range. -Each successful result includes source identity, parameters and deterministic state where applicable. Most operations are synchronous and bounded; custom word-list identity uses the asynchronous browser Web Crypto digest API without a worker. Custom lists are trimmed, emptied lines are dropped and uniqueness is checked once before both selection and metadata are derived. The application contains no third-party network client; its CSP limits connections to its own origin for loading the local application shell. +`random/weighted.ts` parses bounded two-column quoted CSV and ranks each item by an exponential-race key, yielding an exact weighted sample without replacement. `random/recipes.ts` validates a 2 MiB, schema-versioned executable recipe and runs it only through the deterministic source; every algorithm then applies its normal count/list/alphabet bounds. Recipe output includes the generator identity and state after execution. + +`random/ceremony.ts` creates 32-byte WebCrypto reveal nonces. A SHA-256 commitment is domain-separated and binds the normalized ceremony ID, participant name and canonical unpadded Base64url nonce. Finalization validates every commitment, rejects duplicate/missing/changed entries, sorts normalized participant names by a locale-independent code-unit order, and hashes the complete verified reveal set under a separate final-seed domain. It cannot enforce the social publication order. + +Each successful result includes source identity, parameters and deterministic state where applicable. Most operations are synchronous and bounded; custom word-list identity and ceremony hashes use the asynchronous browser Web Crypto digest API without a worker. Custom lists are trimmed, emptied lines are dropped and uniqueness is checked once before both selection and metadata are derived. Weighted input is capped at 100,000 items/4,000,000 UTF-16 units; recipes and ceremony documents at 2 MiB; ceremonies at 1,000 participants. The application contains no third-party network client; its CSP limits connections to its own origin for loading the local application shell. diff --git a/docs/PRIVACY-SECURITY.md b/docs/PRIVACY-SECURITY.md index 99e2b5b..43e2a5d 100644 --- a/docs/PRIVACY-SECURITY.md +++ b/docs/PRIVACY-SECURITY.md @@ -2,6 +2,8 @@ Local Web Crypto is the default. It stays in the browser, has no network or seeded fallback, and is the only mode intended for secrets. Seeded xoshiro128** output is reproducible and explicitly non-cryptographic; anyone with the seed/state/recipe can reproduce or predict it. The entropy estimate for a passphrase is a simple uniform-choice model, not a password-strength audit. -There is no third-party request path, telemetry, analytics, account, geolocation lookup or persistence. Coordinates are generated as mathematical samples and are not derived from the device location. Results and seeds remain in page memory unless copied or downloaded. A custom passphrase recipe contains the normalized list count and a versioned SHA-256 identity, not the list contents; the same normalized input is therefore still required for reproduction. +There is no third-party request path, telemetry, analytics, account, geolocation lookup or persistence. Coordinates are generated as mathematical samples and are not derived from the device location. Results, seeds, recipe contents and ceremony nonces remain in page memory unless copied or downloaded. Executable sample recipes embed their input lists; a custom passphrase recipe instead contains the normalized list count and a versioned SHA-256 identity, not the list contents. The same normalized passphrase input is therefore still required for reproduction. + +Commit–reveal is a coordination primitive, not an audited drawing service. Keep each nonce private until every commitment is independently published, preserve that publication record, and reject missing participants. The verifier proves correspondence between the document's commitments/reveals and derives an order-independent seed; it cannot prove publication timing, participant identity or freedom from collusion. Use that final seed in the deterministic generator and preserve the exact executable recipe for reproducibility. Counts, ranges, alphabets, list sizes and generated byte counts have explicit limits before large allocations. Web Crypto is a browser-provided CSPRNG, not a physical randomness source. The app is not certified for regulated drawings or gambling. diff --git a/package-lock.json b/package-lock.json index 55fd820..1ccc62b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "rand-tools", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rand-tools", - "version": "0.1.1", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -42,24 +42,24 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", + "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", - "license": "GPL-3.0-or-later", - "engines": { - "node": ">=22" - } + "version": "0.2.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.2.0/toolbox-helpers-0.2.0.tgz", + "integrity": "sha512-SdOqkw+P+3J3fa5iVkzb5P15rVepB001GNV21Oh8w0CZcVL+YRltgD/s+MVcTyrNijWQf3E5vtQON/3N2LLyKg==", + "license": "GPL-3.0-or-later" }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -67,17 +67,16 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" - }, - "engines": { - "node": ">=20" } }, "node_modules/@adobe/css-tools": { diff --git a/package.json b/package.json index 2cd7e06..c6401ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rand-tools", - "version": "0.1.1", + "version": "0.2.0", "description": "Generate secure or reproducible random values locally in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,14 +39,14 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", diff --git a/playwright.config.ts b/playwright.config.ts index 731da62..2ab6a22 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index 82263c6..7f25888 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Added exact weighted sampling without replacement and versioned executable seeded recipes. +- Added a bounded, domain-separated WebCrypto commit–reveal ceremony and verifier. + ## 0.1.1 - 2026-09-01 - Removed the external randomness-service client and its network permission; Random Tools is now entirely local-only. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index 45a7c95..4f412f1 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- diff --git a/public/README.md b/public/README.md index c80678b..cd7b063 100644 --- a/public/README.md +++ b/public/README.md @@ -4,16 +4,19 @@ Generate secure or reproducible random values locally in the browser. Random Tools is a standalone local-first application in the [add·ideas Toolbox](https://git.add-ideas.de/lotobo/toolbox-portal). Inputs are processed in the browser and are not uploaded. -## Version 0.1 scope +## Current scope - Unbiased local WebCrypto integers and strings plus normal-distribution samples - Explicitly non-cryptographic, seeded deterministic generation with reproduction metadata - Dice expressions, sampling without replacement, Fisher–Yates shuffling and passphrases +- Exact weighted sampling without replacement from bounded quoted CSV input +- Versioned executable seeded recipes for integers, strings, equal-weight samples and weighted samples +- WebCrypto commit–reveal ceremonies with domain-separated commitments, 256-bit private nonces, canonical participant ordering and fail-closed verification - UUIDv4, UUIDv7 and random ULID generation - Local draws for coin flips, shuffled card deals and unique integer sequences - Random calendar dates with weekday and uniqueness controls, exact decimal fractions, and equal-area spherical coordinates -All functionality runs locally. Secure generation never falls back to the seeded source, and deterministic results are reproducible but not suitable for secrets. Recipes for custom passphrases identify the normalized list by count and SHA-256 but do not embed it, so reproduction requires the same custom input. The browser CSPRNG is not physical entropy, and no operation is presented as certified for regulated drawings or gambling. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). +All functionality runs locally. Secure generation never falls back to the seeded source, and deterministic results/recipes are reproducible but not suitable for secrets. Recipes can embed list values, so review them before sharing. Recipes for custom passphrases identify the normalized list by count and SHA-256 but do not embed it, so reproduction requires the same custom input. A commit–reveal result proves only that the supplied reveals match the supplied commitments; participants must publish every commitment before any reveal through a channel of their choice. The browser CSPRNG is not physical entropy, and no operation is presented as certified for regulated drawings or gambling. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/PRIVACY-SECURITY.md](docs/PRIVACY-SECURITY.md). ## Development @@ -27,7 +30,7 @@ npm run test:browser ## Release -`npm run release:artifact` creates a deterministic `release/rand-tools-0.1.1.zip` and checksum sidecar. +`npm run release:artifact` creates a deterministic `release/rand-tools-0.2.0.zip` and checksum sidecar. ## Licence diff --git a/public/SOURCE.md b/public/SOURCE.md index 43795c7..31da0fb 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Random Tools 0.1.1 is available at: +The corresponding source for Random Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/rand-tools/src/tag/v0.1.1 +https://git.add-ideas.de/lotobo/rand-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md index 36eb28a..a657618 100644 --- a/public/THIRD_PARTY_NOTICES.md +++ b/public/THIRD_PARTY_NOTICES.md @@ -4,9 +4,9 @@ Random Tools 0.1.1 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/public/docs/ARCHITECTURE.md b/public/docs/ARCHITECTURE.md index 296606b..53ee53c 100644 --- a/public/docs/ARCHITECTURE.md +++ b/public/docs/ARCHITECTURE.md @@ -4,4 +4,8 @@ Random Tools is a static React/Vite application wrapped in the shared Toolbox sh `random/generators.ts` builds bounded numbers, strings, identifiers, dice, samples, shuffles, passphrases and normal samples on that interface. `random/draws.ts` adds local binary coin flips, standard playing-card deals, shuffled inclusive integer sequences, Gregorian calendar-date sampling, exact decimal-digit fractions and equal-area points on a spherical surface model. Date sampling can filter weekdays and use a partial Fisher–Yates mapping for selection without replacement; it does not allocate every date in a large range. -Each successful result includes source identity, parameters and deterministic state where applicable. Most operations are synchronous and bounded; custom word-list identity uses the asynchronous browser Web Crypto digest API without a worker. Custom lists are trimmed, emptied lines are dropped and uniqueness is checked once before both selection and metadata are derived. The application contains no third-party network client; its CSP limits connections to its own origin for loading the local application shell. +`random/weighted.ts` parses bounded two-column quoted CSV and ranks each item by an exponential-race key, yielding an exact weighted sample without replacement. `random/recipes.ts` validates a 2 MiB, schema-versioned executable recipe and runs it only through the deterministic source; every algorithm then applies its normal count/list/alphabet bounds. Recipe output includes the generator identity and state after execution. + +`random/ceremony.ts` creates 32-byte WebCrypto reveal nonces. A SHA-256 commitment is domain-separated and binds the normalized ceremony ID, participant name and canonical unpadded Base64url nonce. Finalization validates every commitment, rejects duplicate/missing/changed entries, sorts normalized participant names by a locale-independent code-unit order, and hashes the complete verified reveal set under a separate final-seed domain. It cannot enforce the social publication order. + +Each successful result includes source identity, parameters and deterministic state where applicable. Most operations are synchronous and bounded; custom word-list identity and ceremony hashes use the asynchronous browser Web Crypto digest API without a worker. Custom lists are trimmed, emptied lines are dropped and uniqueness is checked once before both selection and metadata are derived. Weighted input is capped at 100,000 items/4,000,000 UTF-16 units; recipes and ceremony documents at 2 MiB; ceremonies at 1,000 participants. The application contains no third-party network client; its CSP limits connections to its own origin for loading the local application shell. diff --git a/public/docs/PRIVACY-SECURITY.md b/public/docs/PRIVACY-SECURITY.md index 99e2b5b..43e2a5d 100644 --- a/public/docs/PRIVACY-SECURITY.md +++ b/public/docs/PRIVACY-SECURITY.md @@ -2,6 +2,8 @@ Local Web Crypto is the default. It stays in the browser, has no network or seeded fallback, and is the only mode intended for secrets. Seeded xoshiro128** output is reproducible and explicitly non-cryptographic; anyone with the seed/state/recipe can reproduce or predict it. The entropy estimate for a passphrase is a simple uniform-choice model, not a password-strength audit. -There is no third-party request path, telemetry, analytics, account, geolocation lookup or persistence. Coordinates are generated as mathematical samples and are not derived from the device location. Results and seeds remain in page memory unless copied or downloaded. A custom passphrase recipe contains the normalized list count and a versioned SHA-256 identity, not the list contents; the same normalized input is therefore still required for reproduction. +There is no third-party request path, telemetry, analytics, account, geolocation lookup or persistence. Coordinates are generated as mathematical samples and are not derived from the device location. Results, seeds, recipe contents and ceremony nonces remain in page memory unless copied or downloaded. Executable sample recipes embed their input lists; a custom passphrase recipe instead contains the normalized list count and a versioned SHA-256 identity, not the list contents. The same normalized passphrase input is therefore still required for reproduction. + +Commit–reveal is a coordination primitive, not an audited drawing service. Keep each nonce private until every commitment is independently published, preserve that publication record, and reject missing participants. The verifier proves correspondence between the document's commitments/reveals and derives an order-independent seed; it cannot prove publication timing, participant identity or freedom from collusion. Use that final seed in the deterministic generator and preserve the exact executable recipe for reproducibility. Counts, ranges, alphabets, list sizes and generated byte counts have explicit limits before large allocations. Web Crypto is a browser-provided CSPRNG, not a physical randomness source. The app is not certified for regulated drawings or gambling. diff --git a/public/sw.js b/public/sw.js index 0be2904..16b6a8d 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ const CACHE_PREFIX = "rand-tools-shell-"; -const CACHE_NAME = CACHE_PREFIX + "0.1.1"; +const CACHE_NAME = CACHE_PREFIX + "0.2.0"; const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/public/toolbox-app.json b/public/toolbox-app.json index 6470807..2029298 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "id": "de.add-ideas.rand-tools", "name": "Random Tools", - "version": "0.1.1", + "version": "0.2.0", "description": "Generate secure or reproducible random values locally in the browser.", "entry": "./", "icon": "./favicon.svg", @@ -31,6 +31,19 @@ "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { "mediaType": "text/csv", "extensions": [".csv"] }, + { "mediaType": "application/json", "extensions": [".json"] }, + { "mediaType": "text/plain", "extensions": [".txt"] } + ], + "produces": [ + { "mediaType": "application/json", "extensions": [".json"] }, + { "mediaType": "text/csv", "extensions": [".csv"] }, + { "mediaType": "text/plain", "extensions": [".txt"] } + ] + }, + "capabilities": { "required": ["secure-random"], "optional": [] }, "privacy": { "processing": "local", "fileUploads": false, diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index c4f9b04..f21ea46 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -24,6 +24,18 @@ import { randomDates, } from "../random/draws"; import { randomSource, type SourceMode } from "../random/source"; +import { + createCeremonyReveal, + finalizeCeremony, + parseCeremonyDocument, + type CeremonyResult, + type CeremonyReveal, +} from "../random/ceremony"; +import { parseSeededRecipe, runSeededRecipe } from "../random/recipes"; +import { + parseWeightedItems, + weightedSampleWithoutReplacement, +} from "../random/weighted"; import { APP_VERSION } from "../version"; type Tab = @@ -33,7 +45,9 @@ type Tab = | "dice" | "lists" | "passphrases" - | "draws"; + | "draws" + | "recipes" + | "ceremony"; type DrawKind = "coins" | "cards" | "sequence" | "dates" | "decimals" | "coordinates"; const WEEKDAYS = [ @@ -175,6 +189,10 @@ export function Workbench() { const [list, setList] = useState( "amber\nbirch\ncedar\ndelta\nember\nforest\ngranite\nharbor", ); + const [listMode, setListMode] = useState<"equal" | "weighted">("equal"); + const [weightedList, setWeightedList] = useState( + 'Amber,1\nBirch,2\n"Cedar, western",4\nDelta,8', + ); const [sampleCount, setSampleCount] = useState(3); const [shuffle, setShuffle] = useState(false); const [wordCount, setWordCount] = useState(6); @@ -190,6 +208,34 @@ export function Workbench() { const [uniqueDates, setUniqueDates] = useState(false); const [decimalPlaces, setDecimalPlaces] = useState(8); const [coordinatePlaces, setCoordinatePlaces] = useState(6); + const [recipeSource, setRecipeSource] = useState( + JSON.stringify( + { + schemaVersion: 1, + algorithm: "integers-v1", + seed: "reproducible-example", + parameters: { count: 6, minimum: 1, maximumInclusive: 49 }, + }, + null, + 2, + ), + ); + const [ceremonyId, setCeremonyId] = useState("example-draw-2026-09-01"); + const [participant, setParticipant] = useState("Participant A"); + const [ceremonyReveal, setCeremonyReveal] = useState(); + const [ceremonyDocument, setCeremonyDocument] = useState( + JSON.stringify( + { + schemaVersion: 1, + ceremonyId: "example-draw-2026-09-01", + participants: [], + }, + null, + 2, + ), + ); + const [ceremonyResult, setCeremonyResult] = useState(); + const [ceremonyError, setCeremonyError] = useState(""); const activeOperation = useRef(0); type LocalResult = { @@ -299,6 +345,30 @@ export function Workbench() { }); const generateList = () => local((source) => { + if (listMode === "weighted") { + if (shuffle) + throw new Error( + "Weighted mode produces a sample; switch to equal weights to shuffle every item.", + ); + const items = parseWeightedItems(weightedList); + const selected = weightedSampleWithoutReplacement( + source, + items, + sampleCount, + ); + return { + title: "Weighted sample without replacement", + text: selected.map((item) => item.value).join("\n"), + parameters: { + algorithm: + "exponential-race weighted sample without replacement-v1", + inputCount: items.length, + sampleCount, + items, + }, + note: "Weights affect relative selection chances at each draw. Every input row can be selected at most once; duplicate labels on distinct rows remain distinct entries.", + }; + } if (list.length > 4_000_000) throw new Error("List input exceeds 4,000,000 UTF-16 units."); const values = list @@ -322,6 +392,79 @@ export function Workbench() { }, }; }); + + const executeRecipe = () => { + const operationId = ++activeOperation.current; + try { + const recipe = parseSeededRecipe(recipeSource); + const result = runSeededRecipe(recipe); + if (operationId !== activeOperation.current) return; + setOutput({ + title: `Replayed ${recipe.algorithm}`, + text: result.output.join("\n"), + recipe: { + executableRecipe: recipe, + source: result.identity, + sourceClass: "deterministic-non-cryptographic", + stateAfter: result.stateAfter, + }, + note: "Executable recipes are deterministic and reproducible, not suitable for secrets. Review embedded lists before sharing a recipe.", + }); + setError(""); + } catch (reason) { + if (operationId === activeOperation.current) + setError(reason instanceof Error ? reason.message : "Recipe failed."); + } + }; + + const generateCommitment = async () => { + const operationId = ++activeOperation.current; + setCeremonyError(""); + try { + const reveal = await createCeremonyReveal(ceremonyId, participant); + if (operationId === activeOperation.current) setCeremonyReveal(reveal); + } catch (reason) { + if (operationId === activeOperation.current) + setCeremonyError( + reason instanceof Error + ? reason.message + : "Commitment generation failed.", + ); + } + }; + + const useGeneratedReveal = () => { + if (!ceremonyReveal) return; + setCeremonyDocument( + JSON.stringify( + { + schemaVersion: 1, + ceremonyId: ceremonyReveal.ceremonyId, + participants: [ceremonyReveal], + }, + null, + 2, + ), + ); + }; + + const verifyCeremony = async () => { + const operationId = ++activeOperation.current; + setCeremonyError(""); + try { + const result = await finalizeCeremony( + parseCeremonyDocument(ceremonyDocument), + ); + if (operationId === activeOperation.current) setCeremonyResult(result); + } catch (reason) { + if (operationId === activeOperation.current) + setCeremonyError( + reason instanceof Error + ? reason.message + : "Ceremony verification failed.", + ); + } + }; const generatePassphrase = async () => { const operationId = ++activeOperation.current; try { @@ -474,6 +617,8 @@ export function Workbench() { ["lists", "Lists"], ["passphrases", "Passphrases"], ["draws", "Draws"], + ["recipes", "Recipes"], + ["ceremony", "Commit–reveal"], ] as const; return (
@@ -495,16 +640,11 @@ export function Workbench() { seed={seed} setSeed={setSeed} /> -