From 9345d36f4d973f047aa4c6942b6396316e83e29e Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:03:25 +0200 Subject: [PATCH 1/4] test: use native ts --- package.json | 5 +- packages/ts-loader/README.md | 4 -- packages/ts-loader/loader.js | 112 -------------------------------- packages/ts-loader/package.json | 9 --- packages/ts-loader/test.ts | 1 - packages/ts-loader/test2.ts | 7 -- 6 files changed, 1 insertion(+), 137 deletions(-) delete mode 100644 packages/ts-loader/README.md delete mode 100644 packages/ts-loader/loader.js delete mode 100644 packages/ts-loader/package.json delete mode 100644 packages/ts-loader/test.ts delete mode 100644 packages/ts-loader/test2.ts diff --git a/package.json b/package.json index 37afa27..47118e8 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,10 @@ "typescript": "^5.1.3", "typescript-eslint": "^8.34.0" }, - "imports": { - "#ts-loader": "./packages/ts-loader/loader.js" - }, "scripts": { "test:lint": "eslint .", "test:types": "tsc --noEmit", - "test:unit": "node --experimental-global-webcrypto --loader '#ts-loader' test/index.ts", + "test:unit": "node --experimental-global-webcrypto --experimental-strip-types test/index.ts", "test": "yarn test:lint && yarn test:types && yarn test:unit", "build": "tsc --build", "deploy:cli": "rm -rf packages/cli/dist && tsc --build --force && cp README.md packages/cli/. && yarn workspace @node-core/caritat-cli npm publish --access public", diff --git a/packages/ts-loader/README.md b/packages/ts-loader/README.md deleted file mode 100644 index b716035..0000000 --- a/packages/ts-loader/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# TypeScript loader - -This package lets you run Node.js with support for TypeScript. No support for -CommonJS modules. diff --git a/packages/ts-loader/loader.js b/packages/ts-loader/loader.js deleted file mode 100644 index ee9221f..0000000 --- a/packages/ts-loader/loader.js +++ /dev/null @@ -1,112 +0,0 @@ -import { readFile, opendir } from "node:fs/promises"; -import { URL, fileURLToPath } from "node:url"; - -import { transform } from "sucrase"; - -/** - * Maps all the possible import specifier to the absolute URL of the source file. - * - * This is necessary to make sure we load the source file, rather than the maybe - * outdated transpiled file. - * - * @type {Record} - */ -const localPackagesURLs = Object.create(null); -/** - * Maps all the directory names to the name of their local dependencies, plus - * the name their own package. - * - * This is used to send an error if a package tries to load something that's not - * in its dependencies. - * - * @type {Record} - */ -const packagesDirLocalDeps = Object.create(null); - -const ROOT_DIR = new URL("../../packages/", import.meta.url); - -for await (const fsEntry of await opendir(ROOT_DIR)) { - if (fsEntry.isDirectory()) { - const localPackagePackageJsonURL = new URL( - `./${fsEntry.name}/package.json`, - ROOT_DIR - ); - try { - const { name, exports, dependencies } = JSON.parse( - await readFile(localPackagePackageJsonURL, "utf-8") - ); - if (!name) continue; - if (exports) { - for (const [exportedPath, url] of Object.entries(exports)) { - localPackagesURLs[exportedPath.replace(".", () => name)] = new URL( - url.replace(/\/dist\/(.+)\.js$/, "/src/$1.ts"), - localPackagePackageJsonURL - ).href; - } - } - packagesDirLocalDeps[fsEntry.name] = dependencies - ? Object.keys(dependencies).filter((name) => - dependencies[name].startsWith("workspace:") - ) - : []; - // To make self-reference work: - packagesDirLocalDeps[fsEntry.name].push(name); - } catch { - // ignore errors - } - } -} - -const localModuleURLPattern = /\/packages\/([^/]+)\//; - -export async function resolve(urlStr, context, next) { - if (context.parentURL?.startsWith(ROOT_DIR) && urlStr in localPackagesURLs) { - const parentURLPackageDirName = localModuleURLPattern.exec( - context.parentURL - ); - if ( - parentURLPackageDirName != null && - packagesDirLocalDeps[parentURLPackageDirName[1]]?.every( - (depName) => !urlStr.startsWith(depName) - ) - ) { - throw new Error( - `${context.parentURL} tried to import ${urlStr}, which correspond to the local module ${localPackagesURLs[urlStr]}, but it is not listed in its dependencies` - ); - } - urlStr = localPackagesURLs[urlStr]; - } - try { - return await next(urlStr, context); - } catch (err) { - if (err?.code === "ERR_MODULE_NOT_FOUND" && urlStr.endsWith(".js")) { - try { - return await next(urlStr.replace(/\.js$/, ".ts"), context); - } catch (err2) { - if (err2) { - err2.cause = err; - throw err2; - } - throw err; - } - } - throw err; - } -} - -export async function load(urlStr, context, next) { - const url = new URL(urlStr); - if (url.pathname.endsWith(".ts")) { - const { source } = await next(urlStr, { ...context, format: "module" }); - return { - source: transform(source.toString("utf-8"), { - transforms: ["typescript"], - disableESTransforms: true, - filePath: fileURLToPath(url), - }).code, - format: "module", - }; - } else { - return next(urlStr, context); - } -} diff --git a/packages/ts-loader/package.json b/packages/ts-loader/package.json deleted file mode 100644 index d1a58f5..0000000 --- a/packages/ts-loader/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@node-core/caritat-ts-loader", - "private": true, - "version": "0.0.0", - "type": "module", - "dependencies": { - "sucrase": "^3.0.0" - } -} diff --git a/packages/ts-loader/test.ts b/packages/ts-loader/test.ts deleted file mode 100644 index 37ae74e..0000000 --- a/packages/ts-loader/test.ts +++ /dev/null @@ -1 +0,0 @@ -console.log(import.meta.url as string); diff --git a/packages/ts-loader/test2.ts b/packages/ts-loader/test2.ts deleted file mode 100644 index 795c4b5..0000000 --- a/packages/ts-loader/test2.ts +++ /dev/null @@ -1,7 +0,0 @@ -import "./test3.js"; - -import type { Ballot } from "../core/src/vote"; - -const b = {} as Ballot; - -Function.prototype(b); From 37aedcc93686396a022a529e89eaef52a704f0f4 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:06:37 +0200 Subject: [PATCH 2/4] fix --- ...est-environment-node-npm-27.4.6-596a121c86 | 169 ------- jest.config.js | 195 -------- package.json | 5 +- packages/cli/src/bin/generateBallot.ts | 2 +- packages/cli/src/bin/generateNewVoteFolder.ts | 2 +- packages/cli/tsconfig.json | 38 +- packages/core/src/ballotpool.test.ts | 4 +- packages/core/src/chkballot.ts | 4 +- packages/core/src/generateNewVoteFolder.ts | 8 +- packages/core/src/parser.ts | 6 +- packages/core/src/summary/condorcetSummary.ts | 6 +- packages/core/src/summary/electionSummary.ts | 6 +- packages/core/src/vote.ts | 398 ++++++++-------- packages/core/src/voteUsingGit.ts | 4 +- .../src/votingMethods/CondorcetResult.test.ts | 4 +- .../core/src/votingMethods/CondorcetResult.ts | 8 +- .../votingMethods/SingleRoundResult.test.ts | 4 +- .../src/votingMethods/SingleRoundResult.ts | 8 +- packages/core/src/votingMethods/VoteResult.ts | 6 +- packages/core/test/count.test.ts | 6 +- packages/core/test/summary.test.ts | 2 +- packages/core/test/winner.test.ts | 6 +- packages/core/tsconfig.json | 37 +- packages/crypto/src/decrypt.ts | 8 +- packages/crypto/src/deriveKeyIv.ts | 4 +- packages/crypto/src/encrypt.ts | 8 +- packages/crypto/src/generateSplitKeyPair.ts | 8 +- packages/crypto/src/importRsaKey.ts | 4 +- packages/crypto/src/reconstructSplitKey.ts | 4 +- packages/crypto/src/shamir.ts | 424 +++++++++--------- packages/crypto/test/encrypt_decrypt.test.ts | 4 +- .../crypto/test/generateRSAKeyPair.test.ts | 6 +- .../crypto/test/reconstructPrivateKey.test.ts | 8 +- packages/crypto/test/shamir.test.ts | 217 ++++----- packages/crypto/test/tsconfig.json | 36 +- packages/crypto/tsconfig.json | 36 +- 36 files changed, 663 insertions(+), 1032 deletions(-) delete mode 100644 .yarn/patches/jest-environment-node-npm-27.4.6-596a121c86 delete mode 100644 jest.config.js diff --git a/.yarn/patches/jest-environment-node-npm-27.4.6-596a121c86 b/.yarn/patches/jest-environment-node-npm-27.4.6-596a121c86 deleted file mode 100644 index 26357b8..0000000 --- a/.yarn/patches/jest-environment-node-npm-27.4.6-596a121c86 +++ /dev/null @@ -1,169 +0,0 @@ -diff --git a/build/index.js b/build/index.js -index 41401febe67c3430ef9899d7624661e77c38c342..96373735aad753e8bf9f55e00b4b3d263b3e607e 100644 ---- a/build/index.js -+++ b/build/index.js -@@ -1,7 +1,7 @@ --'use strict'; -+"use strict"; - - function _vm() { -- const data = require('vm'); -+ const data = require("vm"); - - _vm = function () { - return data; -@@ -11,7 +11,7 @@ function _vm() { - } - - function _fakeTimers() { -- const data = require('@jest/fake-timers'); -+ const data = require("@jest/fake-timers"); - - _fakeTimers = function () { - return data; -@@ -21,7 +21,7 @@ function _fakeTimers() { - } - - function _jestMock() { -- const data = require('jest-mock'); -+ const data = require("jest-mock"); - - _jestMock = function () { - return data; -@@ -31,7 +31,7 @@ function _jestMock() { - } - - function _jestUtil() { -- const data = require('jest-util'); -+ const data = require("jest-util"); - - _jestUtil = function () { - return data; -@@ -46,7 +46,7 @@ function _defineProperty(obj, key, value) { - value: value, - enumerable: true, - configurable: true, -- writable: true -+ writable: true, - }); - } else { - obj[key] = value; -@@ -56,22 +56,24 @@ function _defineProperty(obj, key, value) { - - class NodeEnvironment { - constructor(config) { -- _defineProperty(this, 'context', void 0); -+ _defineProperty(this, "context", void 0); - -- _defineProperty(this, 'fakeTimers', void 0); -+ _defineProperty(this, "fakeTimers", void 0); - -- _defineProperty(this, 'fakeTimersModern', void 0); -+ _defineProperty(this, "fakeTimersModern", void 0); - -- _defineProperty(this, 'global', void 0); -+ _defineProperty(this, "global", void 0); - -- _defineProperty(this, 'moduleMocker', void 0); -+ _defineProperty(this, "moduleMocker", void 0); - - this.context = (0, _vm().createContext)(); - const global = (this.global = (0, _vm().runInContext)( -- 'this', -+ "this", - Object.assign(this.context, config.testEnvironmentOptions) - )); - global.global = global; -+ global.atob = atob; -+ global.btoa = btoa; - global.clearInterval = clearInterval; - global.clearTimeout = clearTimeout; - global.setInterval = setInterval; -@@ -85,47 +87,47 @@ class NodeEnvironment { - - global.Uint8Array = Uint8Array; // URL and URLSearchParams are global in Node >= 10 - -- if (typeof URL !== 'undefined' && typeof URLSearchParams !== 'undefined') { -+ if (typeof URL !== "undefined" && typeof URLSearchParams !== "undefined") { - global.URL = URL; - global.URLSearchParams = URLSearchParams; - } // TextDecoder and TextDecoder are global in Node >= 11 - - if ( -- typeof TextEncoder !== 'undefined' && -- typeof TextDecoder !== 'undefined' -+ typeof TextEncoder !== "undefined" && -+ typeof TextDecoder !== "undefined" - ) { - global.TextEncoder = TextEncoder; - global.TextDecoder = TextDecoder; - } // queueMicrotask is global in Node >= 11 - -- if (typeof queueMicrotask !== 'undefined') { -+ if (typeof queueMicrotask !== "undefined") { - global.queueMicrotask = queueMicrotask; - } // AbortController is global in Node >= 15 - -- if (typeof AbortController !== 'undefined') { -+ if (typeof AbortController !== "undefined") { - global.AbortController = AbortController; - } // AbortSignal is global in Node >= 15 - -- if (typeof AbortSignal !== 'undefined') { -+ if (typeof AbortSignal !== "undefined") { - global.AbortSignal = AbortSignal; - } // Event is global in Node >= 15.4 - -- if (typeof Event !== 'undefined') { -+ if (typeof Event !== "undefined") { - global.Event = Event; - } // EventTarget is global in Node >= 15.4 - -- if (typeof EventTarget !== 'undefined') { -+ if (typeof EventTarget !== "undefined") { - global.EventTarget = EventTarget; - } // performance is global in Node >= 16 - -- if (typeof performance !== 'undefined') { -+ if (typeof performance !== "undefined") { - global.performance = performance; - } - - (0, _jestUtil().installCommonGlobals)(global, config.globals); - this.moduleMocker = new (_jestMock().ModuleMocker)(global); - -- const timerIdToRef = id => ({ -+ const timerIdToRef = (id) => ({ - id, - - ref() { -@@ -134,24 +136,24 @@ class NodeEnvironment { - - unref() { - return this; -- } -+ }, - }); - -- const timerRefToId = timer => (timer && timer.id) || undefined; -+ const timerRefToId = (timer) => (timer && timer.id) || undefined; - - const timerConfig = { - idToRef: timerIdToRef, -- refToId: timerRefToId -+ refToId: timerRefToId, - }; - this.fakeTimers = new (_fakeTimers().LegacyFakeTimers)({ - config, - global, - moduleMocker: this.moduleMocker, -- timerConfig -+ timerConfig, - }); - this.fakeTimersModern = new (_fakeTimers().ModernFakeTimers)({ - config, -- global -+ global, - }); - } - diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index c72803b..0000000 --- a/jest.config.js +++ /dev/null @@ -1,195 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property, visit: - * https://jestjs.io/docs/configuration - */ - -// All imported modules in your tests should be mocked automatically -export default { - // extensionsToTreatAsEsm: [".ts"], - // automock: false, - - // Stop running tests after `n` failures - // bail: 0, - - // The directory where Jest should store its cached dependency information - // cacheDirectory: "C:\\Users\\steph\\AppData\\Local\\Temp\\jest", - - // Automatically clear mock calls, instances and results before every test - clearMocks: true, - - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // An array of glob patterns indicating a set of files for which coverage information should be collected - // collectCoverageFrom: undefined, - - // The directory where Jest should output its coverage files - coverageDirectory: "coverage", - - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", - - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], - - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: undefined, - - // A path to a custom dependency extractor - // dependencyExtractor: undefined, - - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, - - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], - - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, - - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, - - // A set of global variables that need to be available in all test environments - // globals: {}, - - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", - - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], - - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "jsx", - // "ts", - // "tsx", - // "json", - // "node" - // ], - - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, - - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - modulePathIgnorePatterns: ["\\.ts$"], - - // Activates notifications for test results - // notify: false, - - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", - - // A preset that is used as a base for Jest's configuration - // preset: undefined, - - // Run tests from one or more projects - // projects: undefined, - - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, - - // Automatically reset mock state before every test - // resetMocks: false, - - // Reset the module registry before running each individual test - // resetModules: false, - - // A path to a custom resolver - // resolver: undefined, - - // Automatically restore mock state and implementation before every test - // restoreMocks: false, - - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, - - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "" - // ], - - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", - - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], - - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], - - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, - - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], - - // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", - - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, - - // Adds a location field to test results - // testLocationInResults: false, - - // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], - - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], - - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, - - // This option allows use of a custom test runner - // testRunner: "jest-circus/runner", - - // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href - // testURL: "http://localhost", - - // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" - // timers: "real", - - // A map from regular expressions to paths to transformers - transform: {}, - - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "\\\\node_modules\\\\", - // "\\.pnp\\.[^\\\\]+$" - // ], - - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, - - // Indicates whether each individual test should be reported during the run - // verbose: undefined, - - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], - - // Whether to use watchman for file crawling - // watchman: true, -}; diff --git a/package.json b/package.json index 47118e8..954a273 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,5 @@ "type": "module", "workspaces": [ "packages/*" - ], - "resolutions": { - "jest-environment-node": "patch:jest-environment-node@npm:27.4.6#.yarn/patches/jest-environment-node-npm-27.4.6-596a121c86" - } + ] } diff --git a/packages/cli/src/bin/generateBallot.ts b/packages/cli/src/bin/generateBallot.ts index 24690d0..5b60341 100644 --- a/packages/cli/src/bin/generateBallot.ts +++ b/packages/cli/src/bin/generateBallot.ts @@ -4,7 +4,7 @@ import parseArgs from "../utils/parseArgs.js"; import { loadYmlFile, templateBallot, - VoteFileFormat, + type VoteFileFormat, } from "@node-core/caritat/parser"; const parsedArgs = await parseArgs().options({ diff --git a/packages/cli/src/bin/generateNewVoteFolder.ts b/packages/cli/src/bin/generateNewVoteFolder.ts index e7c25e6..f597fd5 100644 --- a/packages/cli/src/bin/generateNewVoteFolder.ts +++ b/packages/cli/src/bin/generateNewVoteFolder.ts @@ -3,7 +3,7 @@ import { once } from "node:events"; import { stdin, stdout } from "node:process"; -import type { VoteMethod } from "@node-core/caritat/vote.js"; +import type { VoteMethod } from "@node-core/caritat/vote.ts"; import generateNewVoteFolder from "@node-core/caritat/generateNewVoteFolder"; import parseArgs from "../utils/parseArgs.js"; import { getEnv, cliArgs } from "../utils/voteGitEnv.js"; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 49609ab..6f13453 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -1,19 +1,19 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "declaration": true, - "composite": true, - "target": "ESNext", - "module": "ESNext", - "outDir": "./dist", - "moduleResolution": "node", - "rootDir": "./src", - "baseUrl": ".", - "paths": { - "@node-core/caritat/*": ["../core/src/*"], - "@node-core/caritat-crypto/*": ["../crypto/src/*"] - } - }, - "include": ["./src/**/*.ts"], - "references": [{ "path": "../core" }, { "path": "../crypto" }] -} +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "composite": true, + "target": "ESNext", + "module": "ESNext", + "outDir": "./dist", + "moduleResolution": "node", + "rootDir": "./src", + "baseUrl": ".", + "paths": { + "@node-core/caritat/*": ["../core/src/*"], + "@node-core/caritat-crypto/*": ["../crypto/src/*"] + } + }, + "include": ["./src/**/*.ts"], + "references": [{ "path": "../core" }, { "path": "../crypto" }] +} diff --git a/packages/core/src/ballotpool.test.ts b/packages/core/src/ballotpool.test.ts index 59680e5..a2ef11f 100644 --- a/packages/core/src/ballotpool.test.ts +++ b/packages/core/src/ballotpool.test.ts @@ -1,9 +1,9 @@ import it from "node:test"; import { strict as assert } from "node:assert"; -import BallotPoolGit from "./ballotpool.js"; +import BallotPoolGit from "./ballotpool.ts"; -import type { CommitNode } from "./ballotpool.js"; +import type { CommitNode } from "./ballotpool.ts"; const fixturesURL = new URL("../../../test/fixtures/", import.meta.url); diff --git a/packages/core/src/chkballot.ts b/packages/core/src/chkballot.ts index af1b002..febf115 100644 --- a/packages/core/src/chkballot.ts +++ b/packages/core/src/chkballot.ts @@ -1,5 +1,5 @@ -import { checkBallot, loadYmlFile } from "./parser.js"; -import type { BallotFileFormat, VoteFileFormat } from "./parser"; +import { checkBallot, loadYmlFile } from "./parser.ts"; +import type { BallotFileFormat, VoteFileFormat } from "./parser.ts"; function main(argv: string[]): void { const ballotPath = argv[2]; diff --git a/packages/core/src/generateNewVoteFolder.ts b/packages/core/src/generateNewVoteFolder.ts index a2c163a..ae8ce02 100644 --- a/packages/core/src/generateNewVoteFolder.ts +++ b/packages/core/src/generateNewVoteFolder.ts @@ -8,10 +8,10 @@ import { env } from "node:process"; import * as yaml from "js-yaml"; -import { generateAndSplitKeyPair } from "@node-core/caritat-crypto/generateSplitKeyPair"; -import { loadYmlString, templateBallot, VoteFileFormat } from "./parser.js"; -import runChildProcessAsync from "./utils/runChildProcessAsync.js"; -import type { VoteMethod } from "./vote.js"; +import { generateAndSplitKeyPair } from "@node-core/caritat-crypto/generateSplitKeyPair.ts"; +import { loadYmlString, templateBallot, VoteFileFormat } from "./parser.ts"; +import runChildProcessAsync from "./utils/runChildProcessAsync.ts"; +import type { VoteMethod } from "./vote.ts"; interface Options { askForConfirmation?: (ballotContent: string) => boolean | Promise; diff --git a/packages/core/src/parser.ts b/packages/core/src/parser.ts index d5fc26f..660639e 100644 --- a/packages/core/src/parser.ts +++ b/packages/core/src/parser.ts @@ -1,7 +1,7 @@ import * as yaml from "js-yaml"; -import * as fs from "fs"; -import * as crypto from "crypto"; -import { VoteMethod } from "./vote.js"; +import * as fs from "node:fs"; +import * as crypto from "node:crypto"; +import type { VoteMethod } from "./vote.ts"; export interface VoteFileFormat { candidates: string[]; diff --git a/packages/core/src/summary/condorcetSummary.ts b/packages/core/src/summary/condorcetSummary.ts index 5238e7a..b7be829 100644 --- a/packages/core/src/summary/condorcetSummary.ts +++ b/packages/core/src/summary/condorcetSummary.ts @@ -1,6 +1,6 @@ -import { Ballot, VoteCandidate } from "../vote"; -import cleanMarkdown from "../utils/cleanMarkdown.js"; -import ElectionSummary from "./electionSummary.js"; +import type { Ballot, VoteCandidate } from "../vote.ts"; +import cleanMarkdown from "../utils/cleanMarkdown.ts"; +import ElectionSummary from "./electionSummary.ts"; const formatter = new Intl.ListFormat("en", { style: "long", diff --git a/packages/core/src/summary/electionSummary.ts b/packages/core/src/summary/electionSummary.ts index a9ae5c1..62471f5 100644 --- a/packages/core/src/summary/electionSummary.ts +++ b/packages/core/src/summary/electionSummary.ts @@ -1,6 +1,6 @@ -import type { Actor, Ballot, VoteCandidate, VoteCommit } from "../vote"; -import type { CandidateScores } from "../votingMethods/VotingMethodImplementation"; -import cleanMarkdown from "../utils/cleanMarkdown.js"; +import type { Actor, Ballot, VoteCandidate, VoteCommit } from "../vote.ts"; +import type { CandidateScores } from "../votingMethods/VotingMethodImplementation.ts"; +import cleanMarkdown from "../utils/cleanMarkdown.ts"; function displayWinners(winners: VoteCandidate[]) { if (winners.length === 0) return "None."; diff --git a/packages/core/src/vote.ts b/packages/core/src/vote.ts index 2407332..eb8ee70 100644 --- a/packages/core/src/vote.ts +++ b/packages/core/src/vote.ts @@ -1,199 +1,199 @@ -import { - BallotFileFormat, - checkBallot, - loadYmlFile, - loadYmlString, - parseYml, - VoteFileFormat, -} from "./parser.js"; -import type { PathOrFileDescriptor } from "fs"; -import type { CandidateScores } from "./votingMethods/VotingMethodImplementation.js"; -import VoteResult from "./votingMethods/VoteResult.js"; -import CondorcetResult from "./votingMethods/CondorcetResult.js"; -import SingleRoundResult from "./votingMethods/SingleRoundResult.js"; -import { ElectionSummaryOptions } from "./summary/electionSummary.js"; - -export interface Actor { - id: string; -} - -export type VoteCandidate = string; - -export type VoteMethod = - | "MajorityJudgment" - | "Condorcet" - | "InstantRunoff" - | "Scored" - | "SingleRound"; - -export type Rank = number; -export interface Ballot { - voter: Actor; - preferences: Map; -} - -export function getVoteResultImplementation(method: VoteMethod) { - switch (method) { - case "Condorcet": - return CondorcetResult; - case "SingleRound": - return SingleRoundResult; - default: - break; - } - return null as never; -} -export interface VoteCommit { - sha: string; - author: string; - signatureStatus: string; - files: string[]; -} - -export default class Vote { - #candidates: VoteCandidate[]; - #authorizedVoters?: Actor[]; - #votes: Ballot[]; - public subject: string; - private result: CandidateScores; - /** - * Once the voting method is set, trying to change it would probably be vote manipulation, so it can only be set if already null - * Trying to see the result with some voting method while the target is undefined would force it to be said method, for the same reasons - */ - #targetMethod: VoteMethod = null; - - #alreadyCommittedVoters = new Set(); - - private textDecoder = new TextDecoder(); - - public voteFileData: VoteFileFormat = null; - - constructor(options?: { - candidates?: VoteCandidate[]; - authorizedVoters?: Actor[]; - votes?: Ballot[]; - targetMethod?: VoteMethod; - subject?: string; - }) { - this.#candidates = options?.candidates ?? []; - this.#authorizedVoters = options?.authorizedVoters; - this.#votes = options?.votes ?? []; - this.#targetMethod = options?.targetMethod; - this.subject = options?.subject ?? ""; - } - - public loadFromFile(voteFilePath: PathOrFileDescriptor): void { - const voteData: VoteFileFormat = loadYmlFile(voteFilePath); - this.voteFromVoteData(voteData); - } - public loadFromString(voteFileContents: string): void { - const voteData: VoteFileFormat = - loadYmlString(voteFileContents); - this.voteFromVoteData(voteData); - } - private voteFromVoteData(voteData: VoteFileFormat) { - this.voteFileData = voteData; - this.#candidates = voteData.candidates; - this.#targetMethod = voteData.method as VoteMethod; - this.#authorizedVoters = voteData.allowedVoters?.map((id) => ({ id })); - this.subject = voteData.subject ?? "Unknown vote"; - } - - public set targetMethod(method: VoteMethod) { - if (this.#targetMethod == null) { - this.#targetMethod = method; - return; - } - throw new Error("Cannot change the existing target voting method"); - } - - public addCandidate(candidate: VoteCandidate, checkUniqueness = false): void { - if ( - checkUniqueness && - this.#candidates.some( - (existingCandidate: VoteCandidate) => existingCandidate === candidate - ) - ) { - throw new Error("Cannot have duplicate candidate id"); - } - - this.#candidates.push(candidate); - } - - reasonToDiscardCommit(commit: VoteCommit): string { - if (commit.files.length !== 1) - return "This commit touches more than one file."; - if (this.#alreadyCommittedVoters.has(commit.author)) - return "A more recent vote commit from this author has already been counted."; - if ( - this.#authorizedVoters && - !this.#authorizedVoters.some((voter) => voter.id === commit.author) - ) - return `The commit author (${commit.author}) is not in the list of allowed voters.`; - - if ( - this.voteFileData.requireSignedBallots && - commit.signatureStatus !== "G" - ) { - return `Valid signature are required for this vote, expected status G, got ${commit.signatureStatus}`; - } - - this.#alreadyCommittedVoters.add(commit.author); - return null; - } - - public addBallotFile(ballotData: BallotFileFormat, author?: string): Ballot { - if (ballotData && checkBallot(ballotData, this.voteFileData, author)) { - const preferences: Map = new Map( - ballotData.preferences.map((element) => [element.title, element.score]) - ); - const ballot: Ballot = { - voter: { id: author ?? ballotData.author }, - preferences, - }; - this.addBallot(ballot); - return ballot; - } else { - console.warn("Invalid Ballot", author); - } - return null; - } - - public addFakeBallot(author: string): void { - this.addBallot({ voter: { id: author }, preferences: new Map() }); - } - - public addBallotFromBufferSource(data: BufferSource, author?: string): void { - this.addBallotFile( - parseYml(this.textDecoder.decode(data)), - author - ); - } - - public addBallot(ballot: Ballot): void { - const existingBallotIndex = this.#votes.findIndex( - (existingBallot: Ballot) => existingBallot.voter.id === ballot.voter.id - ); - if (existingBallotIndex !== -1) { - this.#votes[existingBallotIndex] = ballot; - return; - } - this.#votes.push(ballot); - } - - public count( - options?: Partial & { method?: VoteMethod } - ): VoteResult { - if (this.#targetMethod == null) throw new Error("Set targetMethod before"); - const VoteResultImpl = getVoteResultImplementation( - options?.method ?? this.#targetMethod - ); - return new VoteResultImpl( - this.#authorizedVoters, - this.#candidates, - this.subject, - this.#votes, - options - ); - } -} +import { + type BallotFileFormat, + checkBallot, + loadYmlFile, + loadYmlString, + parseYml, + type VoteFileFormat, +} from "./parser.ts"; +import type { PathOrFileDescriptor } from "node:fs"; +import type { CandidateScores } from "./votingMethods/VotingMethodImplementation.ts"; +import VoteResult from "./votingMethods/VoteResult.ts"; +import CondorcetResult from "./votingMethods/CondorcetResult.ts"; +import SingleRoundResult from "./votingMethods/SingleRoundResult.ts"; +import type{ ElectionSummaryOptions } from "./summary/electionSummary.ts"; + +export interface Actor { + id: string; +} + +export type VoteCandidate = string; + +export type VoteMethod = + | "MajorityJudgment" + | "Condorcet" + | "InstantRunoff" + | "Scored" + | "SingleRound"; + +export type Rank = number; +export interface Ballot { + voter: Actor; + preferences: Map; +} + +export function getVoteResultImplementation(method: VoteMethod) { + switch (method) { + case "Condorcet": + return CondorcetResult; + case "SingleRound": + return SingleRoundResult; + default: + break; + } + return null as never; +} +export interface VoteCommit { + sha: string; + author: string; + signatureStatus: string; + files: string[]; +} + +export default class Vote { + #candidates: VoteCandidate[]; + #authorizedVoters?: Actor[]; + #votes: Ballot[]; + public subject: string; + private result: CandidateScores; + /** + * Once the voting method is set, trying to change it would probably be vote manipulation, so it can only be set if already null + * Trying to see the result with some voting method while the target is undefined would force it to be said method, for the same reasons + */ + #targetMethod: VoteMethod = null; + + #alreadyCommittedVoters = new Set(); + + private textDecoder = new TextDecoder(); + + public voteFileData: VoteFileFormat = null; + + constructor(options?: { + candidates?: VoteCandidate[]; + authorizedVoters?: Actor[]; + votes?: Ballot[]; + targetMethod?: VoteMethod; + subject?: string; + }) { + this.#candidates = options?.candidates ?? []; + this.#authorizedVoters = options?.authorizedVoters; + this.#votes = options?.votes ?? []; + this.#targetMethod = options?.targetMethod; + this.subject = options?.subject ?? ""; + } + + public loadFromFile(voteFilePath: PathOrFileDescriptor): void { + const voteData: VoteFileFormat = loadYmlFile(voteFilePath); + this.voteFromVoteData(voteData); + } + public loadFromString(voteFileContents: string): void { + const voteData: VoteFileFormat = + loadYmlString(voteFileContents); + this.voteFromVoteData(voteData); + } + private voteFromVoteData(voteData: VoteFileFormat) { + this.voteFileData = voteData; + this.#candidates = voteData.candidates; + this.#targetMethod = voteData.method as VoteMethod; + this.#authorizedVoters = voteData.allowedVoters?.map((id) => ({ id })); + this.subject = voteData.subject ?? "Unknown vote"; + } + + public set targetMethod(method: VoteMethod) { + if (this.#targetMethod == null) { + this.#targetMethod = method; + return; + } + throw new Error("Cannot change the existing target voting method"); + } + + public addCandidate(candidate: VoteCandidate, checkUniqueness = false): void { + if ( + checkUniqueness && + this.#candidates.some( + (existingCandidate: VoteCandidate) => existingCandidate === candidate + ) + ) { + throw new Error("Cannot have duplicate candidate id"); + } + + this.#candidates.push(candidate); + } + + reasonToDiscardCommit(commit: VoteCommit): string { + if (commit.files.length !== 1) + return "This commit touches more than one file."; + if (this.#alreadyCommittedVoters.has(commit.author)) + return "A more recent vote commit from this author has already been counted."; + if ( + this.#authorizedVoters && + !this.#authorizedVoters.some((voter) => voter.id === commit.author) + ) + return `The commit author (${commit.author}) is not in the list of allowed voters.`; + + if ( + this.voteFileData.requireSignedBallots && + commit.signatureStatus !== "G" + ) { + return `Valid signature are required for this vote, expected status G, got ${commit.signatureStatus}`; + } + + this.#alreadyCommittedVoters.add(commit.author); + return null; + } + + public addBallotFile(ballotData: BallotFileFormat, author?: string): Ballot { + if (ballotData && checkBallot(ballotData, this.voteFileData, author)) { + const preferences: Map = new Map( + ballotData.preferences.map((element) => [element.title, element.score]) + ); + const ballot: Ballot = { + voter: { id: author ?? ballotData.author }, + preferences, + }; + this.addBallot(ballot); + return ballot; + } else { + console.warn("Invalid Ballot", author); + } + return null; + } + + public addFakeBallot(author: string): void { + this.addBallot({ voter: { id: author }, preferences: new Map() }); + } + + public addBallotFromBufferSource(data: BufferSource, author?: string): void { + this.addBallotFile( + parseYml(this.textDecoder.decode(data)), + author + ); + } + + public addBallot(ballot: Ballot): void { + const existingBallotIndex = this.#votes.findIndex( + (existingBallot: Ballot) => existingBallot.voter.id === ballot.voter.id + ); + if (existingBallotIndex !== -1) { + this.#votes[existingBallotIndex] = ballot; + return; + } + this.#votes.push(ballot); + } + + public count( + options?: Partial & { method?: VoteMethod } + ): VoteResult { + if (this.#targetMethod == null) throw new Error("Set targetMethod before"); + const VoteResultImpl = getVoteResultImplementation( + options?.method ?? this.#targetMethod + ); + return new VoteResultImpl( + this.#authorizedVoters, + this.#candidates, + this.subject, + this.#votes, + options + ); + } +} diff --git a/packages/core/src/voteUsingGit.ts b/packages/core/src/voteUsingGit.ts index 507f2a8..95a0902 100644 --- a/packages/core/src/voteUsingGit.ts +++ b/packages/core/src/voteUsingGit.ts @@ -10,11 +10,11 @@ import { getGPGSignGitFlag } from "./utils/gpgSign.js"; import encryptData from "@node-core/caritat-crypto/encrypt"; import { - BallotFileFormat, + type BallotFileFormat, loadYmlFile, parseYml, templateBallot, - VoteFileFormat, + type VoteFileFormat, } from "./parser.js"; import { getSummarizedBallot, diff --git a/packages/core/src/votingMethods/CondorcetResult.test.ts b/packages/core/src/votingMethods/CondorcetResult.test.ts index 0a70f29..80a57b7 100644 --- a/packages/core/src/votingMethods/CondorcetResult.test.ts +++ b/packages/core/src/votingMethods/CondorcetResult.test.ts @@ -1,8 +1,8 @@ import { describe, it } from "node:test"; import { strict as assert } from "node:assert"; -import CondorcetVote from "./CondorcetResult.js"; -import type { Actor } from "src/vote.js"; +import CondorcetVote from "./CondorcetResult.ts"; +import type { Actor } from "src/vote.ts"; function condorcet(a, b) { return new CondorcetVote(null as Actor[], a, "subject", b, {}).result; diff --git a/packages/core/src/votingMethods/CondorcetResult.ts b/packages/core/src/votingMethods/CondorcetResult.ts index aacca68..75fc81d 100644 --- a/packages/core/src/votingMethods/CondorcetResult.ts +++ b/packages/core/src/votingMethods/CondorcetResult.ts @@ -1,7 +1,7 @@ -import type { ElectionSummaryOptions } from "../summary/electionSummary"; -import type { Actor, Ballot, VoteCandidate } from "../vote"; -import VoteResult from "./VoteResult.js"; -import type { CandidateScores } from "./VotingMethodImplementation"; +import type { ElectionSummaryOptions } from "../summary/electionSummary.ts"; +import type { Actor, Ballot, VoteCandidate } from "../vote.ts"; +import VoteResult from "./VoteResult.ts"; +import type { CandidateScores } from "./VotingMethodImplementation.ts"; export default class CondorcetResult extends VoteResult { #result: CandidateScores; diff --git a/packages/core/src/votingMethods/SingleRoundResult.test.ts b/packages/core/src/votingMethods/SingleRoundResult.test.ts index 8375a1f..5fd59d6 100644 --- a/packages/core/src/votingMethods/SingleRoundResult.test.ts +++ b/packages/core/src/votingMethods/SingleRoundResult.test.ts @@ -1,8 +1,8 @@ import it from "node:test"; import { strict as assert } from "node:assert"; -import SingleRound from "./SingleRoundResult.js"; -import type { Actor, Ballot } from "../vote.js"; +import SingleRound from "./SingleRoundResult.ts"; +import type { Actor, Ballot } from "../vote.ts"; function singleRound(a: string[], b: Ballot[]) { return new SingleRound(null as Actor[], a, "subject", b, {}).result; diff --git a/packages/core/src/votingMethods/SingleRoundResult.ts b/packages/core/src/votingMethods/SingleRoundResult.ts index ed52606..31520bc 100644 --- a/packages/core/src/votingMethods/SingleRoundResult.ts +++ b/packages/core/src/votingMethods/SingleRoundResult.ts @@ -1,7 +1,7 @@ -import type { ElectionSummaryOptions } from "../summary/electionSummary"; -import type { Actor, Ballot, VoteCandidate } from "../vote"; -import VoteResult from "./VoteResult.js"; -import type { CandidateScores } from "./VotingMethodImplementation"; +import type { ElectionSummaryOptions } from "../summary/electionSummary.ts"; +import type { Actor, Ballot, VoteCandidate } from "../vote.ts"; +import VoteResult from "./VoteResult.ts"; +import type { CandidateScores } from "./VotingMethodImplementation.ts"; export default class SingleRoundResult extends VoteResult { #result: CandidateScores; diff --git a/packages/core/src/votingMethods/VoteResult.ts b/packages/core/src/votingMethods/VoteResult.ts index 336d49f..7333c90 100644 --- a/packages/core/src/votingMethods/VoteResult.ts +++ b/packages/core/src/votingMethods/VoteResult.ts @@ -1,6 +1,6 @@ -import CondorcetElectionSummary from "../summary/condorcetSummary.js"; -import type { ElectionSummaryOptions } from "../summary/electionSummary.js"; -import type { Actor, Ballot, VoteCandidate } from "../vote.js"; +import CondorcetElectionSummary from "../summary/condorcetSummary.ts"; +import type { ElectionSummaryOptions } from "../summary/electionSummary.ts"; +import type { Actor, Ballot, VoteCandidate } from "../vote.ts"; export type CandidateScores = Map; diff --git a/packages/core/test/count.test.ts b/packages/core/test/count.test.ts index 6a56057..cedfcdd 100644 --- a/packages/core/test/count.test.ts +++ b/packages/core/test/count.test.ts @@ -1,9 +1,9 @@ import it from "node:test"; import { strict as assert } from "node:assert"; -import { BallotPool } from "../src/ballotpool.js"; -import { loadYmlFile } from "../src/parser.js"; -import type { VoteFileFormat } from "../src/parser"; +import { BallotPool } from "../src/ballotpool.ts"; +import { loadYmlFile } from "../src/parser.ts"; +import type { VoteFileFormat } from "../src/parser.ts"; const fixturesURL = new URL("../../../test/fixtures/", import.meta.url); diff --git a/packages/core/test/summary.test.ts b/packages/core/test/summary.test.ts index a5fd0e6..5919178 100644 --- a/packages/core/test/summary.test.ts +++ b/packages/core/test/summary.test.ts @@ -1,7 +1,7 @@ import it from "node:test"; import { strict as assert } from "node:assert"; -import CondorcetSummary from "../src/summary/condorcetSummary.js"; +import CondorcetSummary from "../src/summary/condorcetSummary.ts"; const participants = [{ id: "a" }, { id: "b" }, { id: "c" }]; const winners = ["Option 2", "Option 3"]; diff --git a/packages/core/test/winner.test.ts b/packages/core/test/winner.test.ts index 730dd33..ebd0ead 100644 --- a/packages/core/test/winner.test.ts +++ b/packages/core/test/winner.test.ts @@ -1,9 +1,9 @@ import it from "node:test"; import { strict as assert } from "node:assert"; -import { VoteCandidate } from "../src/vote.js"; -import VoteResult from "../src/votingMethods/VoteResult.js"; -import type { CandidateScores } from "../src/votingMethods/VoteResult"; +import type { VoteCandidate } from "../src/vote.ts"; +import VoteResult from "../src/votingMethods/VoteResult.ts"; +import type { CandidateScores } from "../src/votingMethods/VoteResult.ts"; function findWinners( result: CandidateScores diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 6ec64cd..f6afd17 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,18 +1,19 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "declaration": true, - "composite": true, - "target": "ESNext", - "module": "ESNext", - "outDir": "./dist", - "moduleResolution": "node", - "rootDir": "./src", - "baseUrl": ".", - "paths": { - "@node-core/caritat-crypto/*": ["../crypto/src/*"] - } - }, - "include": ["./src/**/*.ts"], - "references": [{ "path": "../crypto" }] -} +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "declaration": true, + "composite": true, + "target": "ESNext", + "module": "ESNext", + "outDir": "./dist", + "moduleResolution": "node", + "rootDir": "./src", + "baseUrl": ".", + "paths": { + "@node-core/caritat-crypto/*": ["../crypto/src/*"] + } + }, + "include": ["./src/**/*.ts"], + "references": [{ "path": "../crypto" }] +} diff --git a/packages/crypto/src/decrypt.ts b/packages/crypto/src/decrypt.ts index daa27a1..5a4bc29 100644 --- a/packages/crypto/src/decrypt.ts +++ b/packages/crypto/src/decrypt.ts @@ -1,8 +1,8 @@ -import { ASYMMETRIC_ALGO, SYMMETRIC_ALGO } from "./config.js"; -import deriveKeyIv from "./deriveKeyIv.js"; -import importRsaKey from "./importRsaKey.js"; +import { ASYMMETRIC_ALGO, SYMMETRIC_ALGO } from "./config.ts"; +import deriveKeyIv from "./deriveKeyIv.ts"; +import importRsaKey from "./importRsaKey.ts"; -import { subtle } from "./webcrypto.js"; +import { subtle } from "./webcrypto.ts"; export async function asymmetricDecrypt( ciphertext: BufferSource, diff --git a/packages/crypto/src/deriveKeyIv.ts b/packages/crypto/src/deriveKeyIv.ts index 73f696b..1e89e7f 100644 --- a/packages/crypto/src/deriveKeyIv.ts +++ b/packages/crypto/src/deriveKeyIv.ts @@ -1,6 +1,6 @@ -import { KEY_DERIVATION_ALGO, SYMMETRIC_ALGO } from "./config.js"; +import { KEY_DERIVATION_ALGO, SYMMETRIC_ALGO } from "./config.ts"; -import { subtle } from "./webcrypto.js"; +import { subtle } from "./webcrypto.ts"; export default async function deriveKeyIv( secret: BufferSource, diff --git a/packages/crypto/src/encrypt.ts b/packages/crypto/src/encrypt.ts index 221d747..270409f 100644 --- a/packages/crypto/src/encrypt.ts +++ b/packages/crypto/src/encrypt.ts @@ -1,8 +1,8 @@ -import { ASYMMETRIC_ALGO, SYMMETRIC_ALGO } from "./config.js"; -import deriveKeyIv from "./deriveKeyIv.js"; -import importRsaKey from "./importRsaKey.js"; +import { ASYMMETRIC_ALGO, SYMMETRIC_ALGO } from "./config.ts"; +import deriveKeyIv from "./deriveKeyIv.ts"; +import importRsaKey from "./importRsaKey.ts"; -import crypto, { subtle } from "./webcrypto.js"; +import crypto, { subtle } from "./webcrypto.ts"; const MAGIC_NUMBER = [ 83, // 'S' diff --git a/packages/crypto/src/generateSplitKeyPair.ts b/packages/crypto/src/generateSplitKeyPair.ts index eb92632..9f10650 100644 --- a/packages/crypto/src/generateSplitKeyPair.ts +++ b/packages/crypto/src/generateSplitKeyPair.ts @@ -1,8 +1,8 @@ -import { ASYMMETRIC_ALGO } from "./config.js"; +import { ASYMMETRIC_ALGO } from "./config.ts"; -import { symmetricEncrypt } from "./encrypt.js"; -import crypto from "./webcrypto.js"; -import * as shamir from "./shamir.js"; +import { symmetricEncrypt } from "./encrypt.ts"; +import crypto from "./webcrypto.ts"; +import * as shamir from "./shamir.ts"; export async function generateRSAKeyPair() { const { privateKey, publicKey } = await crypto.subtle.generateKey( diff --git a/packages/crypto/src/importRsaKey.ts b/packages/crypto/src/importRsaKey.ts index ed6ee54..b6a59c3 100644 --- a/packages/crypto/src/importRsaKey.ts +++ b/packages/crypto/src/importRsaKey.ts @@ -1,6 +1,6 @@ -import { ASYMMETRIC_ALGO } from "./config.js"; +import { ASYMMETRIC_ALGO } from "./config.ts"; -import { subtle } from "./webcrypto.js"; +import { subtle } from "./webcrypto.ts"; const textDecoder = new TextDecoder(); diff --git a/packages/crypto/src/reconstructSplitKey.ts b/packages/crypto/src/reconstructSplitKey.ts index 62220e2..5157768 100644 --- a/packages/crypto/src/reconstructSplitKey.ts +++ b/packages/crypto/src/reconstructSplitKey.ts @@ -1,5 +1,5 @@ -import { symmetricDecrypt } from "./decrypt.js"; -import * as shamir from "./shamir.js"; +import { symmetricDecrypt } from "./decrypt.ts"; +import * as shamir from "./shamir.ts"; export default async function reconstructPrivateKey( encryptedPrivateKey: BufferSource, diff --git a/packages/crypto/src/shamir.ts b/packages/crypto/src/shamir.ts index 0fb778c..e06dc94 100644 --- a/packages/crypto/src/shamir.ts +++ b/packages/crypto/src/shamir.ts @@ -1,212 +1,212 @@ -import crypto from "./webcrypto.js"; - -/** - * u8 represents an element in GF(2^8), which is a representation of a 7-degree polynomial. - * Examples: - * - 0b10101010 represents X^7+X^5+X^3+X, in which X can be either 0 or 1. - * - 0b10011001 represents X^7+X^4+X^3+1, in which X can be either 0 or 1. - * - * u8 coincides with a byte, which is convenient for implementation purposes. - */ -type u8 = number; - -const BITS = 8; -const ORDER_OF_GALOIS_FIELD = 2 ** BITS; -const MAX_VALUE = ORDER_OF_GALOIS_FIELD - 1; -// https://www.partow.net/programming/polynomials/index.html#deg08 -const DEGREE_8_PRIMITIVE_POLYNOMIALS = [ - 0b1_0001_1101, 0b101011, 0b1011111, 0b1100011, 0b1100101, 0b1101001, - 0b11000011, 0b11100111, -]; -const PRIMITIVE = DEGREE_8_PRIMITIVE_POLYNOMIALS[0]; - -const logs = Array(ORDER_OF_GALOIS_FIELD); -const exps = Array(MAX_VALUE); // The set of non-zero elements in GF(q) is an abelian group under the multiplication, of order q – 1. - -// Algorithm to generate lookup tables for corresponding exponential and logarithm in GF(2^8). -// Those lookup tables are necessary to perform polynomial divisions. It is also -// used to speed up the multiplication operation. -for (let i = 0, x = 1; i < MAX_VALUE; i++) { - exps[i] = x; - logs[x] = i; - // X+0 is a generator of GF(2^8), meaning that all non-zero elements in - // GF(2^8) can be expressed as (X+0)^k mod 2^8. - x *= 0b10; - // x is order 7 or less, we multiply by (X+0) which is order 1, so we are - // guaranteed to have x of order 8 or less. To get the back into GF(2^8), we - // need to substract the PRIMITIVE if x>2^8. - if (x >= ORDER_OF_GALOIS_FIELD) { - // if deg(X) >= BITS - x ^= PRIMITIVE; - // P(X) = P(X) + PRIMITIVE(X) mod 2^8 = P(X) - PRIMITIVE mod 2^8 - } -} - -/** - * Multiply two polynomials in GF(2^8). This is equal to - * (a*b)^PRIMITIVE when (a*b)≥256, else it's equal to (a*b). - * @see https://en.wikipedia.org/wiki/Finite_field_arithmetic#Generator_based_tables - */ -function multiplyPolynomials(a: u8, b: u8): u8 { - if (a === 0 || b === 0) return 0; - // a*b = exp(log(a)+log(b)) - return exps[(logs[a] + logs[b]) % MAX_VALUE]; -} - -function dividePolynomials(a: u8, b: u8): u8 { - if (b === 0) throw new RangeError("Div/0"); - if (a === 0) return 0; - // a/b = exp(log(a)-log(b)) - return exps[(logs[a] + MAX_VALUE - logs[b]) % MAX_VALUE]; -} - -/** - * Addition in ℤ/2ℤ is a xor. Therefore, for polynomials on ℤ/2ℤ, it is the same as a - * bitwise xor. - * @see https://en.wikipedia.org/wiki/Finite_field_arithmetic#Addition_and_subtraction - */ -function addPolynomials(a: u8, b: u8): u8 { - return a ^ b; -} - -/** - * The inverse of xor is xor. - */ -const subtractPolynomials = addPolynomials; - -/** - * Hides the secret behind one point per shareholder. - * @param secret A single byte of the secret. If the secret is larger than a - * byte, call this method once per byte. - * @param shareHolders Number of points to generate. - * @param neededParts The minimal number of points that should be necessary to - * reconstruct the secret. - */ -export function* generatePoints(secret: u8, shareHolders: u8, neededParts: u8) { - if (shareHolders > MAX_VALUE) - throw new RangeError( - `Expected ${shareHolders} <= ${MAX_VALUE}. Cannot have more than ` + - `shareholders the size of the Gallois field` - ); - if (shareHolders < neededParts) - throw new RangeError( - `Expected ${shareHolders} < ${neededParts}. Cannot have more less shareholders than needed parts` - ); - // Generate neededParts-1 random polynomial coefficients in GF(2^8) - const coefficients = crypto.getRandomValues(new Uint8Array(neededParts - 1)); - - for (let x = 1; x <= shareHolders; x++) { - // Horner method for fast polynomial evaluation: ax²+bx+c = ((0*x+a)x+b)x+c - let y = coefficients[0]; - for (let t = 1; t < neededParts - 1; t++) { - y = addPolynomials(multiplyPolynomials(x, y), coefficients[t]); - } - // We set the secret as the constant term. - yield { x, y: addPolynomials(multiplyPolynomials(x, y), secret) }; - } -} - -/** - * Generate a byte from points using Lagrange interpolation at the origin - * @param points Points that were given to shareholders. - * @param neededParts If known, the amount of points necessary to reconstruct the secret. - * @returns The secret byte, in clear. - */ -export function reconstructByte( - points: { x: u8; y: u8 }[], - neededParts: u8 = points.length -): u8 { - let Σ = 0; - for (let j = 0; j < neededParts; j++) { - const { x: xj, y: yj } = points[j]; - let Π = 1; - // Evaluate Lagrange polynomial for point j at x0=0. - // It is the only polynomial on GF(2^8) whose value is 1 for x = xj, - // and 0 for all the other points x values - for (let i = 0; i < neededParts; i++) { - if (j === i) continue; - const { x } = points[i]; - Π = multiplyPolynomials( - Π, - dividePolynomials(x, subtractPolynomials(xj, x)) - ); - // Π *= (x0-x)/(xj-x) - // d(X) = (X-x)/(xj-x) is the only line passing through (xj,1) and (x,0) - } - // Scale and add Lagrange polynomials together to get the value of the - // interpolating polynomial at x0=0. - Σ = addPolynomials(Σ, multiplyPolynomials(yj, Π)); - // Σ += yj*Π - } - return Σ; -} - -/** - * Generates one key-part per shareholder, hiding the secret. The secret cannot - * be guessed from shareholders, unless they can provide at least `neededParts` - * key-parts. - * @param data The secret to hide. - * @param shareHolders Number of key-parts to generate. - * @param neededParts The minimal number of key-parts that should be necessary to - * reconstruct the secret. - */ -export function split( - data: BufferSource, - shareHolders: u8, - neededParts: u8 -): Uint8Array[] { - const isView = ArrayBuffer.isView(data); - const length = data.byteLength; - const rawDataView = new DataView( - isView ? data.buffer : data, - isView ? data.byteOffset : 0, - length - ); - - const points = Array.from({ length }, (_, i) => - // Always use GF(2^8), so each chunk needs to be 8 bit long - Array.from( - generatePoints(rawDataView.getUint8(i), shareHolders, neededParts) - ) - ); - return Array.from({ length: shareHolders }, (_, i) => { - const part = new Uint8Array(1 + length); // The +1 here is because we need - // to store the abscisse for each parts. - part[length] = i + 1; // The +1 here is because the abscisse of 0 (the - // constant term) is reserved for storing the secret. - for (let j = 0; j < length; j++) { - part[j] = points[j][i].y; - } - return part; - }); -} - -/** - * Generates the full key using the key parts. - * @param parts Key parts from the shareholders. - * @param neededParts If known, the amount of points necessary to reconstruct the secret. - * @returns The original secret. - */ -export function reconstruct( - parts: BufferSource[], - neededParts: u8 = parts.length -): Uint8Array { - if (parts.length < neededParts) - throw new Error("Not enough parts to reconstruct key"); - const bytes = parts[0].byteLength - 1; - const result = new Uint8Array(bytes); - const dataViews = parts.map((part) => - ArrayBuffer.isView(part) - ? new DataView(part.buffer, part.byteOffset, bytes + 1) - : new DataView(part) - ); - for (let i = 0; i < bytes; i++) { - result[i] = reconstructByte( - Array.from({ length: neededParts }, (_, j) => { - return { x: dataViews[j].getUint8(bytes), y: dataViews[j].getUint8(i) }; - }) - ); - } - - return result; -} +import crypto from "./webcrypto.ts"; + +/** + * u8 represents an element in GF(2^8), which is a representation of a 7-degree polynomial. + * Examples: + * - 0b10101010 represents X^7+X^5+X^3+X, in which X can be either 0 or 1. + * - 0b10011001 represents X^7+X^4+X^3+1, in which X can be either 0 or 1. + * + * u8 coincides with a byte, which is convenient for implementation purposes. + */ +type u8 = number; + +const BITS = 8; +const ORDER_OF_GALOIS_FIELD = 2 ** BITS; +const MAX_VALUE = ORDER_OF_GALOIS_FIELD - 1; +// https://www.partow.net/programming/polynomials/index.html#deg08 +const DEGREE_8_PRIMITIVE_POLYNOMIALS = [ + 0b1_0001_1101, 0b101011, 0b1011111, 0b1100011, 0b1100101, 0b1101001, + 0b11000011, 0b11100111, +]; +const PRIMITIVE = DEGREE_8_PRIMITIVE_POLYNOMIALS[0]; + +const logs = Array(ORDER_OF_GALOIS_FIELD); +const exps = Array(MAX_VALUE); // The set of non-zero elements in GF(q) is an abelian group under the multiplication, of order q – 1. + +// Algorithm to generate lookup tables for corresponding exponential and logarithm in GF(2^8). +// Those lookup tables are necessary to perform polynomial divisions. It is also +// used to speed up the multiplication operation. +for (let i = 0, x = 1; i < MAX_VALUE; i++) { + exps[i] = x; + logs[x] = i; + // X+0 is a generator of GF(2^8), meaning that all non-zero elements in + // GF(2^8) can be expressed as (X+0)^k mod 2^8. + x *= 0b10; + // x is order 7 or less, we multiply by (X+0) which is order 1, so we are + // guaranteed to have x of order 8 or less. To get the back into GF(2^8), we + // need to substract the PRIMITIVE if x>2^8. + if (x >= ORDER_OF_GALOIS_FIELD) { + // if deg(X) >= BITS + x ^= PRIMITIVE; + // P(X) = P(X) + PRIMITIVE(X) mod 2^8 = P(X) - PRIMITIVE mod 2^8 + } +} + +/** + * Multiply two polynomials in GF(2^8). This is equal to + * (a*b)^PRIMITIVE when (a*b)≥256, else it's equal to (a*b). + * @see https://en.wikipedia.org/wiki/Finite_field_arithmetic#Generator_based_tables + */ +function multiplyPolynomials(a: u8, b: u8): u8 { + if (a === 0 || b === 0) return 0; + // a*b = exp(log(a)+log(b)) + return exps[(logs[a] + logs[b]) % MAX_VALUE]; +} + +function dividePolynomials(a: u8, b: u8): u8 { + if (b === 0) throw new RangeError("Div/0"); + if (a === 0) return 0; + // a/b = exp(log(a)-log(b)) + return exps[(logs[a] + MAX_VALUE - logs[b]) % MAX_VALUE]; +} + +/** + * Addition in ℤ/2ℤ is a xor. Therefore, for polynomials on ℤ/2ℤ, it is the same as a + * bitwise xor. + * @see https://en.wikipedia.org/wiki/Finite_field_arithmetic#Addition_and_subtraction + */ +function addPolynomials(a: u8, b: u8): u8 { + return a ^ b; +} + +/** + * The inverse of xor is xor. + */ +const subtractPolynomials = addPolynomials; + +/** + * Hides the secret behind one point per shareholder. + * @param secret A single byte of the secret. If the secret is larger than a + * byte, call this method once per byte. + * @param shareHolders Number of points to generate. + * @param neededParts The minimal number of points that should be necessary to + * reconstruct the secret. + */ +export function* generatePoints(secret: u8, shareHolders: u8, neededParts: u8) { + if (shareHolders > MAX_VALUE) + throw new RangeError( + `Expected ${shareHolders} <= ${MAX_VALUE}. Cannot have more than ` + + `shareholders the size of the Gallois field` + ); + if (shareHolders < neededParts) + throw new RangeError( + `Expected ${shareHolders} < ${neededParts}. Cannot have more less shareholders than needed parts` + ); + // Generate neededParts-1 random polynomial coefficients in GF(2^8) + const coefficients = crypto.getRandomValues(new Uint8Array(neededParts - 1)); + + for (let x = 1; x <= shareHolders; x++) { + // Horner method for fast polynomial evaluation: ax²+bx+c = ((0*x+a)x+b)x+c + let y = coefficients[0]; + for (let t = 1; t < neededParts - 1; t++) { + y = addPolynomials(multiplyPolynomials(x, y), coefficients[t]); + } + // We set the secret as the constant term. + yield { x, y: addPolynomials(multiplyPolynomials(x, y), secret) }; + } +} + +/** + * Generate a byte from points using Lagrange interpolation at the origin + * @param points Points that were given to shareholders. + * @param neededParts If known, the amount of points necessary to reconstruct the secret. + * @returns The secret byte, in clear. + */ +export function reconstructByte( + points: { x: u8; y: u8 }[], + neededParts: u8 = points.length +): u8 { + let Σ = 0; + for (let j = 0; j < neededParts; j++) { + const { x: xj, y: yj } = points[j]; + let Π = 1; + // Evaluate Lagrange polynomial for point j at x0=0. + // It is the only polynomial on GF(2^8) whose value is 1 for x = xj, + // and 0 for all the other points x values + for (let i = 0; i < neededParts; i++) { + if (j === i) continue; + const { x } = points[i]; + Π = multiplyPolynomials( + Π, + dividePolynomials(x, subtractPolynomials(xj, x)) + ); + // Π *= (x0-x)/(xj-x) + // d(X) = (X-x)/(xj-x) is the only line passing through (xj,1) and (x,0) + } + // Scale and add Lagrange polynomials together to get the value of the + // interpolating polynomial at x0=0. + Σ = addPolynomials(Σ, multiplyPolynomials(yj, Π)); + // Σ += yj*Π + } + return Σ; +} + +/** + * Generates one key-part per shareholder, hiding the secret. The secret cannot + * be guessed from shareholders, unless they can provide at least `neededParts` + * key-parts. + * @param data The secret to hide. + * @param shareHolders Number of key-parts to generate. + * @param neededParts The minimal number of key-parts that should be necessary to + * reconstruct the secret. + */ +export function split( + data: BufferSource, + shareHolders: u8, + neededParts: u8 +): Uint8Array[] { + const isView = ArrayBuffer.isView(data); + const length = data.byteLength; + const rawDataView = new DataView( + isView ? data.buffer : data, + isView ? data.byteOffset : 0, + length + ); + + const points = Array.from({ length }, (_, i) => + // Always use GF(2^8), so each chunk needs to be 8 bit long + Array.from( + generatePoints(rawDataView.getUint8(i), shareHolders, neededParts) + ) + ); + return Array.from({ length: shareHolders }, (_, i) => { + const part = new Uint8Array(1 + length); // The +1 here is because we need + // to store the abscisse for each parts. + part[length] = i + 1; // The +1 here is because the abscisse of 0 (the + // constant term) is reserved for storing the secret. + for (let j = 0; j < length; j++) { + part[j] = points[j][i].y; + } + return part; + }); +} + +/** + * Generates the full key using the key parts. + * @param parts Key parts from the shareholders. + * @param neededParts If known, the amount of points necessary to reconstruct the secret. + * @returns The original secret. + */ +export function reconstruct( + parts: BufferSource[], + neededParts: u8 = parts.length +): Uint8Array { + if (parts.length < neededParts) + throw new Error("Not enough parts to reconstruct key"); + const bytes = parts[0].byteLength - 1; + const result = new Uint8Array(bytes); + const dataViews = parts.map((part) => + ArrayBuffer.isView(part) + ? new DataView(part.buffer, part.byteOffset, bytes + 1) + : new DataView(part) + ); + for (let i = 0; i < bytes; i++) { + result[i] = reconstructByte( + Array.from({ length: neededParts }, (_, j) => { + return { x: dataViews[j].getUint8(bytes), y: dataViews[j].getUint8(i) }; + }) + ); + } + + return result; +} diff --git a/packages/crypto/test/encrypt_decrypt.test.ts b/packages/crypto/test/encrypt_decrypt.test.ts index 3ee67fc..c5f2308 100644 --- a/packages/crypto/test/encrypt_decrypt.test.ts +++ b/packages/crypto/test/encrypt_decrypt.test.ts @@ -3,8 +3,8 @@ import { strict as assert } from "node:assert"; import * as fs from "node:fs"; import * as crypto from "node:crypto"; -import encryptBallot from "@node-core/caritat-crypto/encrypt"; -import decryptBallot from "@node-core/caritat-crypto/decrypt"; +import encryptBallot from "../src/encrypt.ts"; +import decryptBallot from "../src/decrypt.ts"; const fixturesURL = new URL("../../../test/fixtures/", import.meta.url); diff --git a/packages/crypto/test/generateRSAKeyPair.test.ts b/packages/crypto/test/generateRSAKeyPair.test.ts index 697e7e5..59b780d 100644 --- a/packages/crypto/test/generateRSAKeyPair.test.ts +++ b/packages/crypto/test/generateRSAKeyPair.test.ts @@ -1,9 +1,9 @@ import assert from "node:assert"; import { it } from "node:test"; -import { generateRSAKeyPair } from "@node-core/caritat-crypto/generateSplitKeyPair"; -import encryptData from "@node-core/caritat-crypto/encrypt"; -import decryptData, { symmetricDecrypt } from "@node-core/caritat-crypto/decrypt"; +import { generateRSAKeyPair } from "../src/generateSplitKeyPair.ts"; +import encryptData from "../src/encrypt.ts"; +import decryptData, { symmetricDecrypt } from "../src/decrypt.ts"; it("should generate a key pair alongside a secret", async () => { const obj = await generateRSAKeyPair(); diff --git a/packages/crypto/test/reconstructPrivateKey.test.ts b/packages/crypto/test/reconstructPrivateKey.test.ts index 76f480a..2ecca56 100644 --- a/packages/crypto/test/reconstructPrivateKey.test.ts +++ b/packages/crypto/test/reconstructPrivateKey.test.ts @@ -1,10 +1,10 @@ import assert from "node:assert"; import { it } from "node:test"; -import { generateAndSplitKeyPair } from "@node-core/caritat-crypto/generateSplitKeyPair"; -import reconstructPrivateKey from "@node-core/caritat-crypto/reconstructSplitKey"; -import decryptData from "@node-core/caritat-crypto/decrypt"; -import encryptData from "@node-core/caritat-crypto/encrypt"; +import { generateAndSplitKeyPair } from "../src/generateSplitKeyPair.ts"; +import reconstructPrivateKey from "../src/reconstructSplitKey.ts"; +import decryptData from "../src/decrypt.ts"; +import encryptData from "../src/encrypt.ts"; it("should handle no secret splitting", async () => { const { shares, encryptedPrivateKey, publicKey } = diff --git a/packages/crypto/test/shamir.test.ts b/packages/crypto/test/shamir.test.ts index f3141ca..dfad173 100644 --- a/packages/crypto/test/shamir.test.ts +++ b/packages/crypto/test/shamir.test.ts @@ -1,108 +1,109 @@ -import * as shamir from "../src/shamir.js"; -import { it } from "node:test"; -import { strict as assert } from "node:assert"; - -const key = crypto.getRandomValues(new Uint8Array(256)); -const shareHolders = 36; -const neededParts = 3; - -const parts = shamir.split(key.buffer, shareHolders, neededParts); - -it("should reconstruct single byte with enough shareholders", () => { - const byte = key[0]; - const points = Array.from( - shamir.generatePoints(byte, shareHolders, neededParts) - ); - const reconstructed = shamir.reconstructByte(points); - assert.strictEqual(reconstructed, byte); -}); - -it("should not give the whole key to any shareholders", () => { - const byte = key[0]; - - const points = Array.from( - shamir.generatePoints(byte, shareHolders, neededParts) - ); - - let coincidences = 0; - - for (let i = 0; i < shareHolders; i++) { - try { - assert.notStrictEqual(points[i].y, byte); - } catch (err) { - if (err?.operator === "notStrictEqual") coincidences++; - } - } - assert.ok(coincidences < neededParts - 1); -}); - -it("should not generate keys if shareholders is greater than threshold", () => { - const byte = key[0]; - - assert.throws( - () => { - shamir.generatePoints(byte, 256, neededParts).next(); - }, - { - message: - "Expected 256 <= 255. Cannot have more than shareholders the size of the Gallois field", - } - ); -}); - -it("should not generate keys if less shareholders than needed parts", () => { - const byte = key[0]; - - assert.throws( - () => { - shamir.generatePoints(byte, 10, 25).next(); - }, - { - message: - "Expected 10 < 25. Cannot have more less shareholders than needed parts", - } - ); -}); - -it("should reconstruct key from enough shareholders", () => { - const reconstructed = shamir.reconstruct([parts[1], parts[0], parts[5]]); - assert.deepStrictEqual(reconstructed, key); -}); - -it("should fail reconstruct key from not enough shareholders", () => { - const reconstructed = shamir.reconstruct([parts[1], parts[5]]); - assert.notDeepStrictEqual(reconstructed, key); -}); - -it("should fail reconstruct key with duplicate shareholders", () => { - assert.throws( - () => { - shamir.reconstruct([parts[1], parts[5], parts[1]]); - }, - { message: "Div/0" } - ); -}); - -it("should still reconstruct key with too many shareholders", () => { - const reconstructed = shamir.reconstruct(parts); - assert.deepStrictEqual(reconstructed, key); -}); - -it( - "should reconstruct key faster when specifying neededParts", - { skip: (shareHolders as number) === (neededParts as number) }, - () => { - const s1 = performance.now(); - const reconstructed1 = shamir.reconstruct(parts, neededParts); - const t1 = performance.now() - s1; - - const s2 = performance.now(); - const reconstructed2 = shamir.reconstruct(parts); - const t2 = performance.now() - s2; - - assert.deepStrictEqual(reconstructed1, key); - assert.deepStrictEqual(reconstructed2, key); - - assert.ok(t2 >= t1); - } -); +import { it } from "node:test"; +import { strict as assert } from "node:assert"; + +import * as shamir from "../src/shamir.ts"; + +const key = crypto.getRandomValues(new Uint8Array(256)); +const shareHolders = 36; +const neededParts = 3; + +const parts = shamir.split(key.buffer, shareHolders, neededParts); + +it("should reconstruct single byte with enough shareholders", () => { + const byte = key[0]; + const points = Array.from( + shamir.generatePoints(byte, shareHolders, neededParts) + ); + const reconstructed = shamir.reconstructByte(points); + assert.strictEqual(reconstructed, byte); +}); + +it("should not give the whole key to any shareholders", () => { + const byte = key[0]; + + const points = Array.from( + shamir.generatePoints(byte, shareHolders, neededParts) + ); + + let coincidences = 0; + + for (let i = 0; i < shareHolders; i++) { + try { + assert.notStrictEqual(points[i].y, byte); + } catch (err) { + if (err?.operator === "notStrictEqual") coincidences++; + } + } + assert.ok(coincidences < neededParts - 1); +}); + +it("should not generate keys if shareholders is greater than threshold", () => { + const byte = key[0]; + + assert.throws( + () => { + shamir.generatePoints(byte, 256, neededParts).next(); + }, + { + message: + "Expected 256 <= 255. Cannot have more than shareholders the size of the Gallois field", + } + ); +}); + +it("should not generate keys if less shareholders than needed parts", () => { + const byte = key[0]; + + assert.throws( + () => { + shamir.generatePoints(byte, 10, 25).next(); + }, + { + message: + "Expected 10 < 25. Cannot have more less shareholders than needed parts", + } + ); +}); + +it("should reconstruct key from enough shareholders", () => { + const reconstructed = shamir.reconstruct([parts[1], parts[0], parts[5]]); + assert.deepStrictEqual(reconstructed, key); +}); + +it("should fail reconstruct key from not enough shareholders", () => { + const reconstructed = shamir.reconstruct([parts[1], parts[5]]); + assert.notDeepStrictEqual(reconstructed, key); +}); + +it("should fail reconstruct key with duplicate shareholders", () => { + assert.throws( + () => { + shamir.reconstruct([parts[1], parts[5], parts[1]]); + }, + { message: "Div/0" } + ); +}); + +it("should still reconstruct key with too many shareholders", () => { + const reconstructed = shamir.reconstruct(parts); + assert.deepStrictEqual(reconstructed, key); +}); + +it( + "should reconstruct key faster when specifying neededParts", + { skip: (shareHolders as number) === (neededParts as number) }, + () => { + const s1 = performance.now(); + const reconstructed1 = shamir.reconstruct(parts, neededParts); + const t1 = performance.now() - s1; + + const s2 = performance.now(); + const reconstructed2 = shamir.reconstruct(parts); + const t2 = performance.now() - s2; + + assert.deepStrictEqual(reconstructed1, key); + assert.deepStrictEqual(reconstructed2, key); + + assert.ok(t2 >= t1); + } +); diff --git a/packages/crypto/test/tsconfig.json b/packages/crypto/test/tsconfig.json index 49ff2eb..54ff954 100644 --- a/packages/crypto/test/tsconfig.json +++ b/packages/crypto/test/tsconfig.json @@ -1,21 +1,17 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "noEmit": true, - "composite": true, - "target": "ESNext", - "module": "ESNext", - "outDir": "./dist", - "moduleResolution": "node", - "watch": true, - "rootDir": "../..", - "baseUrl": "..", - "paths": { - "@node-core/caritat/*": ["../core/src/*"], - "@node-core/caritat-crypto/*": ["../crypto/src/*"] - } - }, - "include": ["./*.ts"], - "references": [{ "path": "../../.." }] - } +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "noEmit": true, + "composite": true, + "target": "ESNext", + "module": "ESNext", + "outDir": "./dist", + "moduleResolution": "node", + "watch": true, + "rootDir": "../..", + "baseUrl": ".." + }, + "include": ["./*.ts"], + "references": [{ "path": "../../.." }] + } \ No newline at end of file diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index 4005ec3..d7fd1a6 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -1,18 +1,18 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "declaration": true, - "composite": true, - "target": "ESNext", - "module": "ESNext", - "outDir": "./dist", - "moduleResolution": "node", - "rootDir": "./src", - "baseUrl": ".", - "paths": { - "@node-core/caritat/*": ["../core/src/*"], - "@node-core/caritat-crypto/*": ["../crypto/src/*"] - } - }, - "include": ["./src/**/*.ts"] -} +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "declaration": true, + "composite": true, + "target": "ESNext", + "module": "ESNext", + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": ".", + "paths": { + "@node-core/caritat/*": ["../core/src/*"], + "@node-core/caritat-crypto/*": ["../crypto/src/*"] + } + }, + "include": ["./src/**/*.ts"] +} From d61ba7de7fa6bb0220053504d7634d03e8eb38c7 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:08:21 +0200 Subject: [PATCH 3/4] Update yarn.lock --- yarn.lock | 94 ++----------------------------------------------------- 1 file changed, 2 insertions(+), 92 deletions(-) diff --git a/yarn.lock b/yarn.lock index bcedcbc..4eee4fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -325,7 +325,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.8 resolution: "@jridgewell/gen-mapping@npm:0.3.8" dependencies: @@ -393,14 +393,6 @@ __metadata: languageName: unknown linkType: soft -"@node-core/caritat-ts-loader@workspace:packages/ts-loader": - version: 0.0.0-use.local - resolution: "@node-core/caritat-ts-loader@workspace:packages/ts-loader" - dependencies: - sucrase: "npm:^3.0.0" - languageName: unknown - linkType: soft - "@node-core/caritat@workspace:*, @node-core/caritat@workspace:packages/core": version: 0.0.0-use.local resolution: "@node-core/caritat@workspace:packages/core" @@ -777,13 +769,6 @@ __metadata: languageName: node linkType: hard -"any-promise@npm:^1.0.0": - version: 1.3.0 - resolution: "any-promise@npm:1.3.0" - checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb - languageName: node - linkType: hard - "anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -1012,13 +997,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.0.0": - version: 4.1.1 - resolution: "commander@npm:4.1.1" - checksum: 10/3b2dc4125f387dab73b3294dbcb0ab2a862f9c0ad748ee2b27e3544d25325b7a8cdfbcc228d103a98a716960b14478114a5206b5415bd48cdafa38797891562c - languageName: node - linkType: hard - "commondir@npm:^1.0.1": version: 1.0.1 resolution: "commondir@npm:1.0.1" @@ -1614,7 +1592,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.2.2": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -1931,13 +1909,6 @@ __metadata: languageName: node linkType: hard -"lines-and-columns@npm:^1.1.6": - version: 1.2.4 - resolution: "lines-and-columns@npm:1.2.4" - checksum: 10/0c37f9f7fa212b38912b7145e1cd16a5f3cd34d782441c3e6ca653485d326f58b3caccda66efce1c5812bde4961bbde3374fae4b0d11bf1226152337f3894aa5 - languageName: node - linkType: hard - "locate-character@npm:^3.0.0": version: 3.0.0 resolution: "locate-character@npm:3.0.0" @@ -2187,17 +2158,6 @@ __metadata: languageName: node linkType: hard -"mz@npm:^2.7.0": - version: 2.7.0 - resolution: "mz@npm:2.7.0" - dependencies: - any-promise: "npm:^1.0.0" - object-assign: "npm:^4.0.1" - thenify-all: "npm:^1.0.0" - checksum: 10/8427de0ece99a07e9faed3c0c6778820d7543e3776f9a84d22cf0ec0a8eb65f6e9aee9c9d353ff9a105ff62d33a9463c6ca638974cc652ee8140cd1e35951c87 - languageName: node - linkType: hard - "nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" @@ -2441,13 +2401,6 @@ __metadata: languageName: node linkType: hard -"pirates@npm:^4.0.1": - version: 4.0.7 - resolution: "pirates@npm:4.0.7" - checksum: 10/2427f371366081ae42feb58214f04805d6b41d6b84d74480ebcc9e0ddbd7105a139f7c653daeaf83ad8a1a77214cf07f64178e76de048128fec501eab3305a96 - languageName: node - linkType: hard - "pkg-dir@npm:^4.1.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -2776,24 +2729,6 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:^3.0.0": - version: 3.35.0 - resolution: "sucrase@npm:3.35.0" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.2" - commander: "npm:^4.0.0" - glob: "npm:^10.3.10" - lines-and-columns: "npm:^1.1.6" - mz: "npm:^2.7.0" - pirates: "npm:^4.0.1" - ts-interface-checker: "npm:^0.1.9" - bin: - sucrase: bin/sucrase - sucrase-node: bin/sucrase-node - checksum: 10/bc601558a62826f1c32287d4fdfa4f2c09fe0fec4c4d39d0e257fd9116d7d6227a18309721d4185ec84c9dc1af0d5ec0e05a42a337fbb74fc293e068549aacbe - languageName: node - linkType: hard - "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -2912,24 +2847,6 @@ __metadata: languageName: node linkType: hard -"thenify-all@npm:^1.0.0": - version: 1.6.0 - resolution: "thenify-all@npm:1.6.0" - dependencies: - thenify: "npm:>= 3.1.0 < 4" - checksum: 10/dba7cc8a23a154cdcb6acb7f51d61511c37a6b077ec5ab5da6e8b874272015937788402fd271fdfc5f187f8cb0948e38d0a42dcc89d554d731652ab458f5343e - languageName: node - linkType: hard - -"thenify@npm:>= 3.1.0 < 4": - version: 3.3.1 - resolution: "thenify@npm:3.3.1" - dependencies: - any-promise: "npm:^1.0.0" - checksum: 10/486e1283a867440a904e36741ff1a177faa827cf94d69506f7e3ae4187b9afdf9ec368b3d8da225c192bfe2eb943f3f0080594156bf39f21b57cd1411e2e7f6d - languageName: node - linkType: hard - "tinyglobby@npm:^0.2.12": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" @@ -2967,13 +2884,6 @@ __metadata: languageName: node linkType: hard -"ts-interface-checker@npm:^0.1.9": - version: 0.1.13 - resolution: "ts-interface-checker@npm:0.1.13" - checksum: 10/9f7346b9e25bade7a1050c001ec5a4f7023909c0e1644c5a96ae20703a131627f081479e6622a4ecee2177283d0069e651e507bedadd3904fc4010ab28ffce00 - languageName: node - linkType: hard - "tslib@npm:^2.3.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" From 0238200ecff6eb7bde7f80ee71f05c0d52b706ab Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:19:02 +0200 Subject: [PATCH 4/4] setup: better usage of node:test --- package.json | 3 ++- test/index.ts | 15 --------------- 2 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 test/index.ts diff --git a/package.json b/package.json index 954a273..3d6264e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "scripts": { "test:lint": "eslint .", "test:types": "tsc --noEmit", - "test:unit": "node --experimental-global-webcrypto --experimental-strip-types test/index.ts", + "test:unit": "node --test --experimental-global-webcrypto --experimental-strip-types", + "test:coverage": "node --test --experimental-global-webcrypto --experimental-strip-types --experimental-test-coverage", "test": "yarn test:lint && yarn test:types && yarn test:unit", "build": "tsc --build", "deploy:cli": "rm -rf packages/cli/dist && tsc --build --force && cp README.md packages/cli/. && yarn workspace @node-core/caritat-cli npm publish --access public", diff --git a/test/index.ts b/test/index.ts deleted file mode 100644 index 747a51e..0000000 --- a/test/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import fs from "node:fs/promises"; - -async function* findTestFiles(url) { - for await (const dirent of await fs.opendir(url)) { - if (dirent.name === "node_modules") continue; - - if (dirent.isDirectory()) - yield* findTestFiles(new URL(`${dirent.name}/`, url)); - else if (dirent.name.endsWith(".test.ts")) yield new URL(dirent.name, url); - } -} - -for await (const file of findTestFiles(new URL("../", import.meta.url))) { - await import(file); -}