diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 011f48d..37fc122 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,11 +10,12 @@ on: jobs: testing: - name: Testing on Node v${{ matrix.node }} + name: Testing on Node v${{ matrix.node }} / CRYPTO_PROVIDER_ARG_REQUIRED=${{ matrix.crypto_arg_required }} runs-on: ubuntu-latest strategy: matrix: - node: [20, 18] + node: [ 20, 18 ] + crypto_arg_required: [ true, false ] steps: - name: Checking out uses: actions/checkout@v3 @@ -28,14 +29,24 @@ jobs: - name: Install NPM dependencies run: npm ci + - name: Modify buildSettings.ts for testing + run: sed -i "s/CRYPTO_PROVIDER_ARG_REQUIRED = false/CRYPTO_PROVIDER_ARG_REQUIRED = ${{ matrix.crypto_arg_required }}/g" src/buildSettings.ts + - name: Check lint run: npm run lint - name: Check build run: npm run build + - name: Check build others + if: matrix.crypto_arg_required == false + run: npm run build:others + - name: Run NPM tests run: npm test - name: Check examples - run: npm run examples + if: matrix.crypto_arg_required == false + run: | + npm run examples + npm run examples:facade diff --git a/README.md b/README.md index 11ef426..79ba175 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ Finally, the client can produce the output[s] of the OPRF protocol using the ser const [output] = await client.finalize(finData, evaluation); ``` +### Facade API + +See [examples](examples/facade/index.ts) + ### Development | Task | NPM scripts | diff --git a/bench/group.bench.ts b/bench/group.bench.ts index 475d084..76cdd5e 100644 --- a/bench/group.bench.ts +++ b/bench/group.bench.ts @@ -4,7 +4,7 @@ // at https://opensource.org/licenses/BSD-3-Clause import Benchmark from 'benchmark' -import { Oprf } from '../src/index.js' +import { type CryptoProvider } from '@cloudflare/voprf-ts' function asyncFn(call: CallableFunction) { return { @@ -16,13 +16,13 @@ function asyncFn(call: CallableFunction) { } } -export async function benchGroup(bs: Benchmark.Suite) { +export async function benchGroup(provider: CryptoProvider, bs: Benchmark.Suite) { const te = new TextEncoder() const msg = te.encode('msg') const dst = te.encode('dst') - for (const id of Oprf.Group.supportedGroups) { - const gg = Oprf.Group.fromID(id) + for (const id of provider.Group.supportedGroups) { + const gg = provider.Group.get(id) const k = await gg.randomScalar() const P = gg.mulGen(k) const Q = P.mul(k) diff --git a/bench/index.ts b/bench/index.ts index 9420c8f..8ee1ba1 100644 --- a/bench/index.ts +++ b/bench/index.ts @@ -4,29 +4,48 @@ // at https://opensource.org/licenses/BSD-3-Clause import Benchmark from 'benchmark' +import { webcrypto } from 'node:crypto' + import { benchGroup } from './group.bench.js' import { benchOPRF } from './oprf.bench.js' -import { webcrypto } from 'node:crypto' +import { getCryptoProviders } from './testProviders.js' + +import { type CryptoProvider } from '../src/index.js' if (typeof crypto === 'undefined') { - global.crypto = webcrypto as unknown as Crypto + Object.assign(global, { crypto: webcrypto }) } -async function bench() { +async function bench(provider: CryptoProvider) { const bs = new Benchmark.Suite() + await benchOPRF(provider, bs) + await benchGroup(provider, bs) - await benchOPRF(bs) - await benchGroup(bs) + return new Promise((resolve, reject) => { + bs.on('cycle', (ev: Benchmark.Event) => { + console.log(`${provider.id}/${String(ev.target)}`) + }) + bs.on('error', (event: Benchmark.Event) => { + bs.abort() + reject(new Error(`error: ${String(event.target)}`)) + }) + bs.on('complete', resolve) - bs.on('cycle', (ev: Benchmark.Event) => { - console.log(String(ev.target)) + bs.run({ async: false }) }) +} - bs.run({ async: false }) +async function runBenchmarksSerially() { + try { + for (const provider of getCryptoProviders()) { + await bench(provider) + } + } catch (_e) { + const e = _e as Error + console.log(`Error: ${e.message}`) + console.log(`Stack: ${e.stack}`) + process.exit(1) + } } -bench().catch((e: Error) => { - console.log(`Error: ${e.message}`) - console.log(`Stack: ${e.stack}`) - process.exit(1) -}) +void runBenchmarksSerially() diff --git a/bench/oprf.bench.ts b/bench/oprf.bench.ts index 54030af..72c2c8c 100644 --- a/bench/oprf.bench.ts +++ b/bench/oprf.bench.ts @@ -4,14 +4,16 @@ // at https://opensource.org/licenses/BSD-3-Clause import { + type CryptoProvider, + Oprf, OPRFClient, OPRFServer, - Oprf, - POPRFClient, POPRFServer, + POPRFClient, VOPRFClient, VOPRFServer, generatePublicKey, + getSupportedSuites, randomPrivateKey } from '../src/index.js' @@ -27,36 +29,36 @@ function asyncFn(call: CallableFunction) { } } -export async function benchOPRF(bs: Benchmark.Suite) { +export async function benchOPRF(provider: CryptoProvider, bs: Benchmark.Suite) { const te = new TextEncoder() const input = te.encode('This is the client input') for (const [mode, m] of Object.entries(Oprf.Mode)) { - for (const [suite, id] of Object.entries(Oprf.Suite)) { - const privateKey = await randomPrivateKey(id) - const publicKey = generatePublicKey(id, privateKey) + for (const id of getSupportedSuites(provider.Group)) { + const privateKey = await randomPrivateKey(id, provider) + const publicKey = generatePublicKey(id, privateKey, provider) let server: OPRFServer | VOPRFServer | POPRFServer let client: OPRFClient | VOPRFClient | POPRFClient switch (m) { case Oprf.Mode.OPRF: - server = new OPRFServer(id, privateKey) - client = new OPRFClient(id) + server = new OPRFServer(id, privateKey, provider) + client = new OPRFClient(id, provider) break case Oprf.Mode.VOPRF: - server = new VOPRFServer(id, privateKey) - client = new VOPRFClient(id, publicKey) + server = new VOPRFServer(id, privateKey, provider) + client = new VOPRFClient(id, publicKey, provider) break case Oprf.Mode.POPRF: - server = new POPRFServer(id, privateKey) - client = new POPRFClient(id, publicKey) + server = new POPRFServer(id, privateKey, provider) + client = new POPRFClient(id, publicKey, provider) break } const [finData, evalReq] = await client.blind([input]) const evaluatedElement = await server.blindEvaluate(evalReq) - const prefix = mode + '/' + suite + '/' + const prefix = mode + '/' + id + '/' bs.add( prefix + 'blind ', diff --git a/bench/testProviders.ts b/bench/testProviders.ts new file mode 100644 index 0000000..4fa6d7a --- /dev/null +++ b/bench/testProviders.ts @@ -0,0 +1,16 @@ +import { CryptoNoble } from '../src/cryptoNoble.js' +import { CryptoSjcl } from '../src/cryptoSjcl.js' + +const allProviders = [CryptoNoble, CryptoSjcl] +const providerMatch = process.env.CRYPTO_PROVIDER + +export function getCryptoProviders() { + const names = allProviders.map((p) => p.id) + const testProviders = allProviders.filter( + (provider) => !providerMatch || provider.id === providerMatch + ) + if (testProviders.length === 0) { + throw new Error(`no CryptoProvider with name === ${providerMatch} among [${names}]`) + } + return testProviders +} diff --git a/bench/tsconfig.json b/bench/tsconfig.json index d891cd4..38fdb6d 100644 --- a/bench/tsconfig.json +++ b/bench/tsconfig.json @@ -1,8 +1,13 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "compilerOptions": { + "paths": { + "@cloudflare/voprf-ts": ["../src/index.js"], + "@cloudflare/voprf-ts/crypto-noble": ["../src/cryptoNoble.js"], + "@cloudflare/voprf-ts/facade": ["../src/facade/index.js"] + } + }, + "include": ["."], "references": [ { "path": ".." diff --git a/examples/facade/index.ts b/examples/facade/index.ts new file mode 100644 index 0000000..e0d064b --- /dev/null +++ b/examples/facade/index.ts @@ -0,0 +1,165 @@ +import { CryptoNoble } from '@cloudflare/voprf-ts/crypto-noble' +// You may want to do this when 3rd party dependencies use voprf-ts +// import { Oprf as OprfCore } from '@cloudflare/voprf-ts' +// OprfCore.Crypto = CryptoNoble + +import { webcrypto } from 'node:crypto' + +import { Oprf, type OprfApi, type SuiteID } from '@cloudflare/voprf-ts/facade' + +export async function facadeOprfExample(Oprf: OprfApi, suite: SuiteID = Oprf.Suite.P521_SHA512) { + // Setup: Create client and server. + const mode = Oprf.makeMode({ + mode: Oprf.Mode.OPRF, + suite + }) + const privateKey = await mode.keys.randomPrivate() + + const server = mode.makeServer(privateKey) + const client = mode.makeClient() + + // Client Server + // ==================================================== + // Step 1: The client prepares arbitrary input that will be evaluated by the + // server, the blinding method produces an evaluation request, and some + // finalization data to be used later. Then, the client sends the evaluation + // request to the server. + // + // Client + // blind, blindedElement = Blind(input) + const input = 'This is the client input' + const inputBytes = new TextEncoder().encode(input) + const [finData, evalReq] = await client.blind([inputBytes]) + // evalReq + // ------------------>> + // Server + // Step 2: Once the server received the evaluation request, it responds to + // the client with an evaluation. + // + // evaluation = BlindEvaluate(evalReq, info*) + const evaluation = await server.blindEvaluate(evalReq) + // evaluation.proof <- does not have member + + // evaluation + // <<------------------ + // + // Client + // Step 3: Finally, the client can produce the output of the OPRF protocol + // using the server's evaluation and the finalization data from the first + // step. If the mode is verifiable, this step allows the client to check the + // proof that the server used the expected private key for the evaluation. + // + // output = Finalize(finData, evaluation, info*) + const [output] = await client.finalize(finData, evaluation) + + // Step 4: redemption song! + const verified = await server.verifyFinalize(inputBytes, output) + + console.log(`Example OPRF - SuiteID: ${mode.suite}`) + console.log(`CryptoProvider: ${mode.crypto.id}`) + console.log(`input (${input.length} bytes): ${input}`) + console.log(`output (${output.length} bytes): ${Buffer.from(output).toString('hex')}`) + console.log(`verified: ${verified}\n`) +} + +export async function facadePoprfExample(Oprf: OprfApi, suite: SuiteID = Oprf.Suite.P256_SHA256) { + // Setup: Create client and server. + const mode = Oprf.makeMode({ + mode: Oprf.Mode.POPRF, + suite + }) + const { privateKey, publicKey } = await mode.keys.generatePair() + + const server = mode.makeServer(privateKey) + const client = mode.makeClient(publicKey) + + // Client Server + // ==================================================== + // Step 1: The client prepares arbitrary input that will be evaluated by the + // server, the blinding method produces an evaluation request, and some + // finalization data to be used later. Then, the client sends the evaluation + // request to the server. + + const input = 'This is the client input' + const info = 'Shared info between server and client' + const inputBytes = new TextEncoder().encode(input) + const infoBytes = new TextEncoder().encode(info) + const [finData, evalReq] = await client.blind([inputBytes]) + + // Step 2: Once the server received the evaluation request, it responds to + // the client with an evaluation. + const evaluation = await server.blindEvaluate(evalReq, infoBytes) + + // Step 3: Finally, the client can produce the output of the OPRF protocol + // using the server's evaluation and the finalization data from the first + // step. If the mode is verifiable, this step allows the client to check the + // proof that the server used the expected private key for the evaluation. + const [output] = await client.finalize(finData, evaluation, infoBytes) + + // Step 4: redemption song! + const verified = await server.verifyFinalize(inputBytes, output, infoBytes) + + console.log(`Example POPRF - SuiteID: ${mode.suite}`) + console.log(`CryptoProvider: ${mode.crypto.id}`) + console.log(`input (${input.length} bytes): ${input}`) + console.log(`output (${output.length} bytes): ${Buffer.from(output).toString('hex')}`) + console.log(`info (${info.length} bytes): ${info}`) + console.log(`verified: ${verified}\n`) +} + +export async function facadeVoprfExample(Oprf: OprfApi, suite: SuiteID = Oprf.Suite.P384_SHA384) { + // Setup: Create client and server. + const mode = Oprf.makeMode({ + mode: Oprf.Mode.VOPRF, + suite + }) + const { privateKey, publicKey } = await mode.keys.generatePair() + + const server = mode.makeServer(privateKey) + const client = mode.makeClient(publicKey) + + // Client Server + // ==================================================== + // Step 1: The client prepares arbitrary input that will be evaluated by the + // server, the blinding method produces an evaluation request, and some + // finalization data to be used later. Then, the client sends the evaluation + // request to the server. + + const input = 'This is the client input' + const inputBytes = new TextEncoder().encode(input) + const [finData, evalReq] = await client.blind([inputBytes]) + + // Step 2: Once the server received the evaluation request, it responds to + // the client with an evaluation. + const evaluation = await server.blindEvaluate(evalReq) + + // Step 3: Finally, the client can produce the output of the OPRF protocol + // using the server's evaluation and the finalization data from the first + // step. If the mode is verifiable, this step allows the client to check the + // proof that the server used the expected private key for the evaluation. + const [output] = await client.finalize(finData, evaluation) + + // Step 4: redemption song! + const verified = await server.verifyFinalize(inputBytes, output) + + console.log(`Example VOPRF - SuiteID: ${mode.suite}`) + console.log(`CryptoProvider: ${mode.crypto.id}`) + console.log(`input (${input.length} bytes): ${input}`) + console.log(`output (${output.length} bytes): ${Buffer.from(output).toString('hex')}\n`) + console.log(`verified: ${verified}\n`) +} + +async function main() { + if (typeof crypto === 'undefined') { + Object.assign(global, { crypto: webcrypto }) + } + await facadeOprfExample(Oprf) + await facadeVoprfExample(Oprf) + + // This suite requires the noble crypto provider as sjcl doesn't support + // ristretto. + const OprfNoble = Oprf.withConfig({ crypto: CryptoNoble }) + await facadePoprfExample(OprfNoble, Oprf.Suite.RISTRETTO255_SHA512) +} + +main().catch(console.error) diff --git a/examples/oprf.ts b/examples/oprf.ts index 97f9cc2..573eb84 100644 --- a/examples/oprf.ts +++ b/examples/oprf.ts @@ -3,7 +3,7 @@ // Licensed under the BSD-3-Clause license found in the LICENSE file or // at https://opensource.org/licenses/BSD-3-Clause -import { OPRFClient, OPRFServer, Oprf, randomPrivateKey } from '../src/index.js' +import { OPRFClient, OPRFServer, Oprf, randomPrivateKey } from '@cloudflare/voprf-ts' // Example: OPRF mode with the P521-SHA512 suite. export async function oprfExample() { diff --git a/examples/poprf.ts b/examples/poprf.ts index 51b2f21..dcb4d8c 100644 --- a/examples/poprf.ts +++ b/examples/poprf.ts @@ -9,7 +9,7 @@ import { POPRFServer, generatePublicKey, randomPrivateKey -} from '../src/index.js' +} from '@cloudflare/voprf-ts' // Example: POPRF mode with the P256_SHA256 suite. export async function poprfExample() { diff --git a/examples/tsconfig.json b/examples/tsconfig.json index d891cd4..38fdb6d 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -1,8 +1,13 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "compilerOptions": { + "paths": { + "@cloudflare/voprf-ts": ["../src/index.js"], + "@cloudflare/voprf-ts/crypto-noble": ["../src/cryptoNoble.js"], + "@cloudflare/voprf-ts/facade": ["../src/facade/index.js"] + } + }, + "include": ["."], "references": [ { "path": ".." diff --git a/examples/voprf.ts b/examples/voprf.ts index f3eee87..67b4d33 100644 --- a/examples/voprf.ts +++ b/examples/voprf.ts @@ -9,7 +9,7 @@ import { VOPRFServer, generatePublicKey, randomPrivateKey -} from '../src/index.js' +} from '@cloudflare/voprf-ts' // Example: VOPRF mode with the P384-SHA384 suite. export async function voprfExample() { diff --git a/package-lock.json b/package-lock.json index f5921b3..5c2546a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@cloudflare/voprf-ts", "version": "0.21.2", + "hasInstallScript": true, "license": "BSD-3-Clause", "devDependencies": { "@noble/curves": "1.2.0", @@ -25,6 +26,7 @@ "jest": "29.7.0", "prettier": "3.0.3", "sjcl": "1.0.8", + "tsx": "^3.13.0", "typescript": "5.2.2" }, "engines": { @@ -657,9 +659,9 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", + "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", @@ -706,6 +708,358 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2738,6 +3092,43 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3282,6 +3673,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4949,6 +5352,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -5367,6 +5779,33 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsx": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.13.0.tgz", + "integrity": "sha512-rjmRpTu3as/5fjNq/kOkOtihgLxuIz6pbKdj9xwP4J5jOLkBxw/rjN5ANw+KyrrOXV5uB7HC8+SrrSJxT65y+A==", + "dev": true, + "dependencies": { + "esbuild": "~0.18.20", + "get-tsconfig": "^4.7.2", + "source-map-support": "^0.5.21" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 9a855f3..946fe91 100644 --- a/package.json +++ b/package.json @@ -1,81 +1,91 @@ { - "name": "@cloudflare/voprf-ts", - "version": "0.21.2", - "description": "voprf-ts: A TypeScript Library for Oblivious Pseudorandom Functions (OPRF)", - "author": "Armando Faz ", - "maintainers": [ - "Armando Faz " - ], - "contributors": [ - "Nicholas Dudfield " - ], - "license": "BSD-3-Clause", - "private": false, - "main": "./lib/cjs/src/index.js", - "module": "./lib/esm/src/index.js", - "types": "./lib/esm/src/index.d.ts", - "exports": { - ".": { - "default": "./lib/cjs/src/index.js", - "require": "./lib/cjs/src/index.js", - "import": "./lib/esm/src/index.js" - }, - "./crypto-noble": { - "require": "./lib/cjs/src/cryptoNoble.js", - "import": "./lib/esm/src/cryptoNoble.js" - } + "name": "@cloudflare/voprf-ts", + "version": "0.21.2", + "description": "voprf-ts: A TypeScript Library for Oblivious Pseudorandom Functions (OPRF)", + "author": "Armando Faz ", + "maintainers": [ + "Armando Faz " + ], + "contributors": [ + "Nicholas Dudfield " + ], + "license": "BSD-3-Clause", + "private": false, + "main": "./lib/cjs/src/index.js", + "module": "./lib/esm/src/index.js", + "types": "./lib/esm/src/index.d.ts", + "exports": { + ".": { + "default": "./lib/cjs/src/index.js", + "require": "./lib/cjs/src/index.js", + "import": "./lib/esm/src/index.js" }, - "files": [ - "lib/**/src/**/!(*.tsbuildinfo)", - "webcrypto.md" - ], - "keywords": [ - "oprf", - "voprf", - "poprf", - "crypto", - "cryptography" - ], - "homepage": "https://github.com/cloudflare/voprf-ts#readme", - "repository": "github:cloudflare/voprf-ts", - "engines": { - "node": ">=18" + "./crypto-noble": { + "default": "./lib/cjs/src/cryptoNoble.js", + "require": "./lib/cjs/src/cryptoNoble.js", + "import": "./lib/esm/src/cryptoNoble.js" }, - "devDependencies": { - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/benchmark": "2.1.3", - "@types/jest": "29.5.5", - "@typescript-eslint/eslint-plugin": "6.7.3", - "@typescript-eslint/parser": "6.7.3", - "benchmark": "2.1.4", - "eslint": "8.50.0", - "eslint-config-prettier": "9.0.0", - "eslint-plugin-jest": "27.4.0", - "eslint-plugin-jest-formatting": "3.1.0", - "eslint-plugin-prettier": "5.0.0", - "eslint-plugin-security": "1.7.1", - "jest": "29.7.0", - "prettier": "3.0.3", - "sjcl": "1.0.8", - "typescript": "5.2.2" - }, - "scripts": { - "prepack": "tsc -b . tsconfig.cjs.json", - "prepare": "tsc -b . tsconfig.cjs.json", - "build": "tsc -b . tsconfig.cjs.json", - "clean": "tsc -b --clean . test ./tsconfig.cjs.json test/tsconfig.cjs.json bench examples", - "test": "npm run test:esm && npm run test:cjs", - "test:esm": "npm run test:build && node --experimental-vm-modules node_modules/.bin/jest --ci --selectProjects esm --coverageDirectory coverage/esm", - "test:cjs": "npm run test:build && node node_modules/.bin/jest --ci --selectProjects cjs --coverageDirectory coverage/cjs", - "test:build": "tsc -b test test/tsconfig.cjs.json", - "examples": "tsc -b examples && node ./lib/esm/examples/index.js", - "lint": "eslint .", - "bench": "tsc -b bench && node ./lib/esm/bench/index.js", - "format": "prettier './(src|test|bench|examples)/**/!(*.d).ts' --write" - }, - "optionalDependencies": { - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2" + "./facade": { + "default": "./lib/cjs/src/facade/index.js", + "require": "./lib/cjs/src/facade/index.js", + "import": "./lib/esm/src/facade/index.js" } + }, + "files": [ + "lib/**/src/**/!(*.tsbuildinfo)", + "webcrypto.md" + ], + "keywords": [ + "oprf", + "voprf", + "poprf", + "crypto", + "cryptography" + ], + "homepage": "https://github.com/cloudflare/voprf-ts#readme", + "repository": "github:cloudflare/voprf-ts", + "engines": { + "node": ">=18" + }, + "devDependencies": { + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/benchmark": "2.1.3", + "@types/jest": "29.5.5", + "@typescript-eslint/eslint-plugin": "6.7.3", + "@typescript-eslint/parser": "6.7.3", + "benchmark": "2.1.4", + "eslint": "8.50.0", + "eslint-config-prettier": "9.0.0", + "eslint-plugin-jest": "27.4.0", + "eslint-plugin-jest-formatting": "3.1.0", + "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-security": "1.7.1", + "jest": "29.7.0", + "prettier": "3.0.3", + "sjcl": "1.0.8", + "tsx": "^3.13.0", + "typescript": "5.2.2" + }, + "scripts": { + "prepack": "tsc -b . tsconfig.cjs.json", + "prepare": "tsc -b . tsconfig.cjs.json", + "build": "tsc -b . tsconfig.cjs.json", + "build:others": "tsc -b bench examples", + "clean": "tsc -b --clean . test ./tsconfig.cjs.json test/tsconfig.cjs.json bench examples", + "test": "npm run test:esm && npm run test:cjs", + "test:esm": "npm run test:build && node --experimental-vm-modules node_modules/.bin/jest --ci --selectProjects esm --coverageDirectory coverage/esm", + "test:cjs": "npm run test:build && node node_modules/.bin/jest --ci --selectProjects cjs --coverageDirectory coverage/cjs", + "test:build": "tsc -b test test/tsconfig.cjs.json", + "examples": "npm run build && tsx examples/index.ts", + "examples:facade": "npm run build && tsx examples/facade/index.ts", + "lint": "eslint .", + "bench": "npm run build && tsx bench/index.ts", + "format": "prettier './(src|test|bench|examples)/**/!(*.d).ts' --write", + "format:json": "prettier './**/*.json' '!./node_modules/**' '!./package-lock.json' '!./test/testdata/allVectors_v20.json' --write" + }, + "optionalDependencies": { + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2" + } } diff --git a/src/buildSettings.ts b/src/buildSettings.ts new file mode 100644 index 0000000..0861931 --- /dev/null +++ b/src/buildSettings.ts @@ -0,0 +1,18 @@ +// This file can be easily written over in CI +import { CryptoSjcl } from './cryptoSjcl.js' + +// See CryptoProviderArg used as ...rest type pervasively +// We can set this to `true` to check that the internal code and +// tests are properly passing around the provider object everywhere. +// We can set it to `false` to not require the arg and let it +// default to `DEFAULT_CRYPTO_PROVIDER` +export const CRYPTO_PROVIDER_ARG_REQUIRED = false + +// Of course, this means that using the api with a non default provider +// is not very nice, but it does allow the facade api to avoid the +// use of a global, and potentially allow the use of multiple providers +// in a single process, picking and choosing a provider for group support. +// You can use setCryptoProvider if you want to use the non-facade api with a +// non-default provider and not need to pass everything around. +// Single provider per process limitations apply. +export const DEFAULT_CRYPTO_PROVIDER = CryptoSjcl diff --git a/src/client.ts b/src/client.ts index aafb5c6..decab7a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3,7 +3,6 @@ // Licensed under the BSD-3-Clause license found in the LICENSE file or // at https://opensource.org/licenses/BSD-3-Clause -import { DLEQVerifier } from './dleq.js' import type { Elt, Scalar } from './groupTypes.js' import { Evaluation, @@ -13,15 +12,18 @@ import { Oprf, type SuiteID } from './oprf.js' + import { zip } from './util.js' +import type { CryptoProviderArg } from './cryptoImpl.js' +import { DLEQVerifier } from './dleq.js' class baseClient extends Oprf { - constructor(mode: ModeID, suite: SuiteID) { - super(mode, suite) + constructor(mode: ModeID, suite: SuiteID, ...arg: CryptoProviderArg) { + super(mode, suite, ...arg) } randomBlinder(): Promise { - return this.gg.randomScalar() + return this.group.randomScalar() } async blind(inputs: Uint8Array[]): Promise<[FinalizeData, EvaluationRequest]> { @@ -29,7 +31,7 @@ class baseClient extends Oprf { const blinds = [] for (const input of inputs) { const scalar = await this.randomBlinder() - const inputElement = await this.gg.hashToGroup( + const inputElement = await this.group.hashToGroup( input, this.getDST(Oprf.LABELS.HashToGroupDST) ) @@ -66,9 +68,10 @@ class baseClient extends Oprf { } export class OPRFClient extends baseClient { - constructor(suite: SuiteID) { - super(Oprf.Mode.OPRF, suite) + constructor(suite: SuiteID, ...arg: CryptoProviderArg) { + super(Oprf.Mode.OPRF, suite, ...arg) } + finalize(finData: FinalizeData, evaluation: Evaluation): Promise> { return super.doFinalize(finData, evaluation) } @@ -77,26 +80,27 @@ export class OPRFClient extends baseClient { export class VOPRFClient extends baseClient { constructor( suite: SuiteID, - private readonly pubKeyServer: Uint8Array + private readonly pubKeyServer: Uint8Array, + ...arg: CryptoProviderArg ) { - super(Oprf.Mode.VOPRF, suite) + super(Oprf.Mode.VOPRF, suite, ...arg) } async finalize(finData: FinalizeData, evaluation: Evaluation): Promise> { if (!evaluation.proof) { throw new Error('no proof provided') } - const pkS = this.gg.desElt(this.pubKeyServer) + const pkS = this.group.desElt(this.pubKeyServer) const n = finData.inputs.length if (evaluation.evaluated.length !== n) { throw new Error('mismatched lengths') } - const verifier = new DLEQVerifier(this.getDLEQParams()) + const verifier = new DLEQVerifier(this.getDLEQParams(), this.crypto) if ( !(await verifier.verify_batch( - [this.gg.generator(), pkS], + [this.group.generator(), pkS], zip(finData.evalReq.blinded, evaluation.evaluated), evaluation.proof )) @@ -111,15 +115,16 @@ export class VOPRFClient extends baseClient { export class POPRFClient extends baseClient { constructor( suite: SuiteID, - private readonly pubKeyServer: Uint8Array + private readonly pubKeyServer: Uint8Array, + ...arg: CryptoProviderArg ) { - super(Oprf.Mode.POPRF, suite) + super(Oprf.Mode.POPRF, suite, ...arg) } private async pointFromInfo(info: Uint8Array): Promise { const m = await this.scalarFromInfo(info) - const T = this.gg.mulGen(m) - const pkS = this.gg.desElt(this.pubKeyServer) + const T = this.group.mulGen(m) + const pkS = this.group.desElt(this.pubKeyServer) const tw = pkS.add(T) if (tw.isIdentity()) { throw new Error('invalid info') @@ -141,10 +146,10 @@ export class POPRFClient extends baseClient { throw new Error('mismatched lengths') } - const verifier = new DLEQVerifier(this.getDLEQParams()) + const verifier = new DLEQVerifier(this.getDLEQParams(), this.crypto) if ( !(await verifier.verify_batch( - [this.gg.generator(), tw], + [this.group.generator(), tw], zip(evaluation.evaluated, finData.evalReq.blinded), evaluation.proof )) diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..7b989e6 --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,22 @@ +export const MODE = { + OPRF: 0, + VOPRF: 1, + POPRF: 2 +} as const + +export const SUITE = { + P256_SHA256: 'P256-SHA256', + P384_SHA384: 'P384-SHA384', + P521_SHA512: 'P521-SHA512', + RISTRETTO255_SHA512: 'ristretto255-SHA512', + DECAF448_SHAKE256: 'decaf448-SHAKE256' +} as const + +export const LABELS = { + Version: 'OPRFV1-', + FinalizeDST: 'Finalize', + HashToGroupDST: 'HashToGroup-', + HashToScalarDST: 'HashToScalar-', + DeriveKeyPairDST: 'DeriveKeyPair', + InfoLabel: 'Info' +} as const diff --git a/src/cryptoImpl.ts b/src/cryptoImpl.ts new file mode 100644 index 0000000..8773f8c --- /dev/null +++ b/src/cryptoImpl.ts @@ -0,0 +1,39 @@ +import { getOprfParams, type SuiteID } from './oprf.js' +import type { CryptoProvider } from './cryptoTypes.js' +import { CRYPTO_PROVIDER_ARG_REQUIRED, DEFAULT_CRYPTO_PROVIDER } from './buildSettings.js' +import type { GroupID } from './groupTypes.js' + +const REQUIRED = CRYPTO_PROVIDER_ARG_REQUIRED +let configured = DEFAULT_CRYPTO_PROVIDER + +type OptionalArg = [cryptoProvider?: CryptoProvider] +type RequiredArg = [CryptoProvider] + +export type CryptoProviderArg = typeof REQUIRED extends true ? RequiredArg : OptionalArg + +export function getCrypto(arg: CryptoProviderArg) { + const [provider] = arg as OptionalArg + if (!provider && REQUIRED) { + throw new Error(`Undefined crypto arg`) + } + + return provider ?? configured +} + +export function getGroup(groupID: GroupID, arg: CryptoProviderArg) { + const provider = getCrypto(arg) + return provider.Group.get(groupID) +} + +export function getSuiteGroup(suite: SuiteID, arg: CryptoProviderArg) { + return getGroup(getOprfParams(suite)[1], arg) +} + +// This way the `old` api can be used +export function setCryptoProvider(provider: CryptoProvider) { + configured = provider +} + +export function getCryptoProvider() { + return configured +} diff --git a/src/cryptoNoble.ts b/src/cryptoNoble.ts index e0d89e6..36ad769 100644 --- a/src/cryptoNoble.ts +++ b/src/cryptoNoble.ts @@ -8,6 +8,7 @@ import type { CryptoProvider, HashID } from './cryptoTypes.js' import { hashSync } from './noble/hashes.js' export const CryptoNoble: CryptoProvider = { + id: 'noble', Group: GroupNb, hash(hashID: HashID, input: Uint8Array): Promise { return Promise.resolve(hashSync(hashID, input)) diff --git a/src/cryptoSjcl.ts b/src/cryptoSjcl.ts index b3d17a1..c04036d 100644 --- a/src/cryptoSjcl.ts +++ b/src/cryptoSjcl.ts @@ -7,6 +7,7 @@ import type { CryptoProvider, HashID } from './cryptoTypes.js' import { GroupConsSjcl } from './groupSjcl.js' export const CryptoSjcl: CryptoProvider = { + id: 'sjcl', Group: GroupConsSjcl, async hash(hashID: HashID, input: Uint8Array): Promise { return new Uint8Array(await crypto.subtle.digest(hashID, input)) diff --git a/src/cryptoTypes.ts b/src/cryptoTypes.ts index a02f5bd..401baae 100644 --- a/src/cryptoTypes.ts +++ b/src/cryptoTypes.ts @@ -8,6 +8,7 @@ import type { GroupCons } from './groupTypes.js' export type HashID = 'SHA-512' | 'SHA-256' | 'SHA-384' | 'SHAKE256' export interface CryptoProvider { + id: string Group: GroupCons hash(hashID: HashID, input: Uint8Array): Promise } diff --git a/src/dleq.ts b/src/dleq.ts index bb76fbd..76c6316 100644 --- a/src/dleq.ts +++ b/src/dleq.ts @@ -5,15 +5,15 @@ // // Implementation of batched discrete log equivalents proofs (DLEQ) as // described in https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-voprf-21#name-discrete-logarithm-equivale -import type { HashID } from './cryptoTypes.js' -import type { Elt, Group, Scalar } from './groupTypes.js' +import type { CryptoProvider, HashID } from './cryptoTypes.js' +import type { Elt, Group, GroupID, Scalar } from './groupTypes.js' import { checkSize, joinAll, to16bits, toU16LenPrefix } from './util.js' +import { type CryptoProviderArg, getCrypto, getGroup } from './cryptoImpl.js' export interface DLEQParams { - readonly group: Group readonly dst: Uint8Array - readonly hashID: HashID - hash(hashID: HashID, input: Uint8Array): Promise + readonly group: GroupID + readonly hash: HashID } const LABELS = { @@ -29,18 +29,22 @@ async function computeComposites( params: DLEQParams, b: Elt, cd: Array<[Elt, Elt]>, - key?: Scalar + key: Scalar | undefined, + ...arg: CryptoProviderArg ): Promise<{ M: Elt; Z: Elt }> { + const crypto = getCrypto(arg) + const group = crypto.Group.get(params.group) + const te = new TextEncoder() const Bm = b.serialize() const seedDST = joinAll([te.encode(LABELS.Seed), params.dst]) const h1Input = joinAll([...toU16LenPrefix(Bm), ...toU16LenPrefix(seedDST)]) - const seed = await params.hash(params.hashID, h1Input) + const seed = await crypto.hash(params.hash, h1Input) const compositeLabel = te.encode(LABELS.Composite) const h2sDST = joinAll([te.encode(LABELS.HashToScalar), params.dst]) - let M = params.group.identity() - let Z = params.group.identity() + let M = group.identity() + let Z = group.identity() let i = 0 for (const [c, d] of cd) { const Ci = c.serialize() @@ -53,7 +57,7 @@ async function computeComposites( ...toU16LenPrefix(Di), compositeLabel ]) - const di = await params.group.hashToScalar(h2Input, h2sDST) + const di = await group.hashToScalar(h2Input, h2sDST) M = M.add(c.mul(di)) if (!key) { @@ -73,7 +77,11 @@ async function computeComposites( // from https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-voprf-21#name-discrete-logarithm-equivale // to generate a challenge from the input elements. The point arguments // correspond to [B, M, Z, t2, t3] from the specification. -function challenge(params: DLEQParams, points: [Elt, Elt, Elt, Elt, Elt]): Promise { +function challenge( + group: Group, + params: DLEQParams, + points: [Elt, Elt, Elt, Elt, Elt] +): Promise { let h2Input = new Uint8Array() for (const p of points) { const P = p.serialize() @@ -82,7 +90,7 @@ function challenge(params: DLEQParams, points: [Elt, Elt, Elt, Elt, Elt]): Promi const te = new TextEncoder() h2Input = joinAll([h2Input, te.encode(LABELS.Challenge)]) const h2sDST = joinAll([te.encode(LABELS.HashToScalar), params.dst]) - return params.group.hashToScalar(h2Input, h2sDST) + return group.hashToScalar(h2Input, h2sDST) } export class DLEQProof { @@ -99,21 +107,31 @@ export class DLEQProof { return joinAll([this.c.serialize(), this.s.serialize()]) } - static size(g: Group): number { - return 2 * g.scalarSize() + static size(group: Group): number { + return 2 * group.scalarSize() } - static deserialize(g: Group, bytes: Uint8Array): DLEQProof { - checkSize(bytes, DLEQProof, g) - const n = g.scalarSize() - const c = g.desScalar(bytes.subarray(0, n)) - const s = g.desScalar(bytes.subarray(n, 2 * n)) + static deserialize(groupID: GroupID, bytes: Uint8Array, ...arg: CryptoProviderArg): DLEQProof { + const group = getGroup(groupID, arg) + checkSize(bytes, DLEQProof, group) + const n = group.scalarSize() + const c = group.desScalar(bytes.subarray(0, n)) + const s = group.desScalar(bytes.subarray(n, 2 * n)) return new DLEQProof(c, s) } } export class DLEQVerifier { - constructor(public readonly params: DLEQParams) {} + readonly crypto: CryptoProvider + readonly group: Group + + constructor( + public readonly params: DLEQParams, + ...arg: CryptoProviderArg + ) { + this.crypto = getCrypto(arg) + this.group = this.crypto.Group.get(params.group) + } verify(p0: [Elt, Elt], p1: [Elt, Elt], proof: DLEQProof): Promise { return this.verify_batch(p0, [p1], proof) @@ -124,10 +142,10 @@ export class DLEQVerifier { // The argument p0 corresponds to the elements A, B, and the argument p1s // corresponds to the arrays of elements C and D from the specification. async verify_batch(p0: [Elt, Elt], p1s: Array<[Elt, Elt]>, proof: DLEQProof): Promise { - const { M, Z } = await computeComposites(this.params, p0[1], p1s) + const { M, Z } = await computeComposites(this.params, p0[1], p1s, undefined, this.crypto) const t2 = p0[0].mul2(proof.s, p0[1], proof.c) const t3 = M.mul2(proof.s, Z, proof.c) - const c = await challenge(this.params, [p0[1], M, Z, t2, t3]) + const c = await challenge(this.group, this.params, [p0[1], M, Z, t2, t3]) return proof.c.isEqual(c) } } @@ -137,8 +155,8 @@ export class DLEQProver extends DLEQVerifier { return this.prove_batch(k, p0, [p1], r) } - randomScalar(): Promise { - return this.params.group.randomScalar() + randomScalar() { + return this.group.randomScalar() } // prove_batch implements the GenerateProof function @@ -152,10 +170,10 @@ export class DLEQProver extends DLEQVerifier { r?: Scalar ): Promise { const rnd = r ? r : await this.randomScalar() - const { M, Z } = await computeComposites(this.params, p0[1], p1s, key) + const { M, Z } = await computeComposites(this.params, p0[1], p1s, key, this.crypto) const t2 = p0[0].mul(rnd) const t3 = M.mul(rnd) - const c = await challenge(this.params, [p0[1], M, Z, t2, t3]) + const c = await challenge(this.group, this.params, [p0[1], M, Z, t2, t3]) const s = rnd.sub(c.mul(key)) return new DLEQProof(c, s) } diff --git a/src/facade/impl/ClientImpl.ts b/src/facade/impl/ClientImpl.ts new file mode 100644 index 0000000..a8cabfb --- /dev/null +++ b/src/facade/impl/ClientImpl.ts @@ -0,0 +1,57 @@ +import type { Client, FinalizeData as FacadeFinalizeData } from '../types.js' +import { OPRFClient, VOPRFClient, POPRFClient } from '../../client.js' +import { MODE } from '../../consts.js' +import { OprfBaseImpl } from './OprfBaseImpl.js' +import { FinalizeData, EvaluationRequest } from '../../oprf.js' + +export class ClientImpl extends OprfBaseImpl implements Client { + spyHandle: Client['spyHandle'] + blind: Client['blind'] + finalize: Client['finalize'] + + constructor( + publicKey: Uint8Array | undefined, + ...args: ConstructorParameters + ) { + super(...args) + let wrapped: OPRFClient | VOPRFClient | POPRFClient + if (this.mode !== MODE.OPRF && !publicKey) { + throw new Error(`public key must be set for VOPRF|POPRF modes`) + } else { + publicKey = publicKey! + } + + switch (this.mode) { + case MODE.OPRF: + wrapped = new OPRFClient(this.suite, this.crypto) + break + case MODE.POPRF: + wrapped = new POPRFClient(this.suite, publicKey, this.crypto) + break + case MODE.VOPRF: + wrapped = new VOPRFClient(this.suite, publicKey, this.crypto) + break + default: + throw new Error(`Unsupported mode: ${this.mode}`) + } + this.blind = async (inputs: Uint8Array[]) => { + const [finData, evalReq] = await wrapped.blind(inputs) + return [finData, this.codec.encodeEvaluationRequest(evalReq)] + } + this.finalize = (finData, evaluation, ...info) => { + return wrapped.finalize( + this.mapFinalizationData(finData), + this.codec.decodeEvaluation({ mode: this.mode, ...evaluation }), + ...info + ) + } + this.spyHandle = { blinds: wrapped } + } + + private mapFinalizationData(fac: FacadeFinalizeData): FinalizeData { + if (fac instanceof FinalizeData) { + return fac + } + return new FinalizeData(fac.inputs, fac.blinds, new EvaluationRequest(fac.evalReq.blinded)) + } +} diff --git a/src/facade/impl/Codec.ts b/src/facade/impl/Codec.ts new file mode 100644 index 0000000..1d5b6c0 --- /dev/null +++ b/src/facade/impl/Codec.ts @@ -0,0 +1,62 @@ +import type { + BaseEvaluation as FacadeEvaluationBase, + VerifiableEvaluation as FacadeVerifiableEvaluation, + Serialized, + EvaluationRequest as FacadeEvaluationRequest, + HasSerialize +} from '../types.js' +import type { Group, Elt, Scalar } from '../../groupTypes.js' +import type { CryptoProvider } from '../../cryptoTypes.js' +import { DLEQProof } from '../../dleq.js' +import { Evaluation, EvaluationRequest } from '../../oprf.js' + +export type FacadeEvaluation = FacadeEvaluationBase & Partial + +export class Codec { + constructor( + private group: Group, + private crypto: CryptoProvider + ) {} + + decodeElts(evaluated: Array>): Elt[] { + return evaluated.map(this.group.desElt.bind(this.group)) + } + + decodeScalars(evaluated: Array>): Scalar[] { + return evaluated.map(this.group.desScalar.bind(this.group)) + } + + encodeArray(array: HasSerialize[]): Uint8Array[] { + return array.map((e) => e.serialize()) + } + + decodeProof(proof: Uint8Array) { + return DLEQProof.deserialize(this.group.id, proof, this.crypto) + } + + encodeEvaluation(lib: Evaluation): FacadeEvaluation { + return { + proof: lib.proof ? lib.proof.serialize() : undefined, + mode: lib.mode, + evaluated: this.encodeArray(lib.evaluated) + } + } + + encodeEvaluationRequest(evalRequest: EvaluationRequest): FacadeEvaluationRequest { + return { + blinded: this.encodeArray(evalRequest.blinded) + } + } + + decodeEvaluation(fac: FacadeEvaluation): Evaluation { + return new Evaluation( + fac.mode, + this.decodeElts(fac.evaluated), + fac.proof ? this.decodeProof(fac.proof) : undefined + ) + } + + decodeEvaluationRequest(fac: FacadeEvaluationRequest) { + return new EvaluationRequest(this.decodeElts(fac.blinded)) + } +} diff --git a/src/facade/impl/KeyManagerImpl.ts b/src/facade/impl/KeyManagerImpl.ts new file mode 100644 index 0000000..15d27c7 --- /dev/null +++ b/src/facade/impl/KeyManagerImpl.ts @@ -0,0 +1,47 @@ +import { OprfBaseImpl } from './OprfBaseImpl.js' +import type { KeySizes, KeyPair, KeyManager } from '../types.js' +import { + getKeySizes, + validatePrivateKey, + validatePublicKey, + randomPrivateKey, + derivePrivateKey, + generatePublicKey, + generateKeyPair, + deriveKeyPair +} from '../../keys.js' + +export class KeyManagerImpl extends OprfBaseImpl implements KeyManager { + sizes(): KeySizes { + const internal = getKeySizes(this.suite, this.crypto) + return { publicKey: internal.Npk, privateKey: internal.Nsk } + } + + validatePrivate(privateKey: Uint8Array): boolean { + return validatePrivateKey(this.suite, privateKey, this.crypto) + } + + validatePublic(publicKey: Uint8Array): boolean { + return validatePublicKey(this.suite, publicKey, this.crypto) + } + + randomPrivate(): Promise { + return randomPrivateKey(this.suite, this.crypto) + } + + derivePrivate(seed: Uint8Array, info: Uint8Array): Promise { + return derivePrivateKey(this.mode, this.suite, seed, info, this.crypto) + } + + generatePublic(privateKey: Uint8Array): Uint8Array { + return generatePublicKey(this.suite, privateKey, this.crypto) + } + + generatePair(): Promise { + return generateKeyPair(this.suite, this.crypto) + } + + derivePair(seed: Uint8Array, info: Uint8Array): Promise { + return deriveKeyPair(this.mode, this.suite, seed, info, this.crypto) + } +} diff --git a/src/facade/impl/ModeImpl.ts b/src/facade/impl/ModeImpl.ts new file mode 100644 index 0000000..6877f1c --- /dev/null +++ b/src/facade/impl/ModeImpl.ts @@ -0,0 +1,52 @@ +import type { Mode, ModeID, SuiteID, Server, Client, KeyManager, ModeParams } from '../types.js' +import type { Group } from '../../groupTypes.js' +import type { CryptoProvider } from '../../cryptoTypes.js' +import { ServerImpl } from './ServerImpl.js' +import { ClientImpl } from './ClientImpl.js' +import { KeyManagerImpl } from './KeyManagerImpl.js' +import { getOprfParams } from '../../oprf.js' + +export class ModeImpl implements Mode { + keys: KeyManager + params: ModeParams + + constructor( + public mode: ModeID, + public suite: SuiteID, + public group: Group, + public crypto: CryptoProvider + ) { + this.keys = new KeyManagerImpl(...this.getBaseArgs()) + this.params = this.getParams() + } + + private getParams() { + const [suite, group, hash, size] = getOprfParams(this.suite) + const scalar = this.group.scalarSize() + const elt = this.group.eltSize() + return { + mode: this.mode, + suite, + group, + hash, + sizes: { + elt: elt, + output: size, + proof: scalar * 2, + scalar + } + } + } + + makeServer(privateKey: Uint8Array): Server { + return new ServerImpl(privateKey, ...this.getBaseArgs()) + } + + makeClient(publicKey?: Uint8Array): Client { + return new ClientImpl(publicKey, ...this.getBaseArgs()) + } + + private getBaseArgs() { + return [this.mode, this.suite, this.crypto, this.group] as const + } +} diff --git a/src/facade/impl/OprfApiImpl.ts b/src/facade/impl/OprfApiImpl.ts new file mode 100644 index 0000000..59db63f --- /dev/null +++ b/src/facade/impl/OprfApiImpl.ts @@ -0,0 +1,26 @@ +import type { OprfApi, ModeID, MakeModeParams, Mode } from '../types.js' +import type { CryptoProvider } from '../../cryptoTypes.js' +import { SUITE, MODE } from '../../consts.js' +import { getOprfParams } from '../../oprf.js' +import { ModeImpl } from './ModeImpl.js' +import { getCryptoProvider } from '../../cryptoImpl.js' + +export class OprfApiImpl implements OprfApi { + constructor(private _crypto?: CryptoProvider) {} + + get crypto() { + return this._crypto ?? getCryptoProvider() + } + + Suite = SUITE + Mode = MODE + + withConfig(config: { crypto: CryptoProvider }): OprfApi { + return new OprfApiImpl(config.crypto) + } + + makeMode(params: MakeModeParams): Mode { + const group = this.crypto.Group.get(getOprfParams(params.suite)[1]) + return new ModeImpl(params.mode, params.suite, group, this.crypto) as Mode as Mode + } +} diff --git a/src/facade/impl/OprfBaseImpl.ts b/src/facade/impl/OprfBaseImpl.ts new file mode 100644 index 0000000..7079040 --- /dev/null +++ b/src/facade/impl/OprfBaseImpl.ts @@ -0,0 +1,15 @@ +import type { ModeID, SuiteID } from '../types.js' +import { Codec } from './Codec.js' +import type { CryptoProvider } from '../../cryptoTypes.js' +import type { Group } from '../../groupTypes.js' + +export class OprfBaseImpl { + protected codec = new Codec(this.group, this.crypto) + + constructor( + public mode: ModeID, + public suite: SuiteID, + public crypto: CryptoProvider, + public group: Group + ) {} +} diff --git a/src/facade/impl/ServerImpl.ts b/src/facade/impl/ServerImpl.ts new file mode 100644 index 0000000..46f711b --- /dev/null +++ b/src/facade/impl/ServerImpl.ts @@ -0,0 +1,39 @@ +import type { Server } from '../types.js' +import { OPRFServer, VOPRFServer, POPRFServer } from '../../server.js' +import { MODE } from '../../consts.js' +import { OprfBaseImpl } from './OprfBaseImpl.js' + +export class ServerImpl extends OprfBaseImpl implements Server { + verifyFinalize: Server['verifyFinalize'] + blindEvaluate: Server['blindEvaluate'] + spyHandle: Server['spyHandle'] + + constructor(privateKey: Uint8Array, ...args: ConstructorParameters) { + super(...args) + let wrapped: OPRFServer | VOPRFServer | POPRFServer + switch (this.mode) { + case MODE.OPRF: + wrapped = new OPRFServer(this.suite, privateKey, this.crypto) + break + case MODE.POPRF: + wrapped = new POPRFServer(this.suite, privateKey, this.crypto) + break + case MODE.VOPRF: + wrapped = new VOPRFServer(this.suite, privateKey, this.crypto) + break + default: + throw new Error(`Unsupported mode: ${this.mode}`) + } + this.spyHandle = { + dleqProver: wrapped['prover'] + } + this.verifyFinalize = wrapped.verifyFinalize.bind(wrapped) + this.blindEvaluate = async (req, ...info) => { + const internal = await wrapped.blindEvaluate( + this.codec.decodeEvaluationRequest(req), + ...info + ) + return this.codec.encodeEvaluation(internal) + } + } +} diff --git a/src/facade/index.ts b/src/facade/index.ts new file mode 100644 index 0000000..09bcbde --- /dev/null +++ b/src/facade/index.ts @@ -0,0 +1,6 @@ +import { OprfApiImpl } from './impl/OprfApiImpl.js' + +export const Oprf = new OprfApiImpl(undefined) +export * from './types.js' +export * from '../cryptoTypes.js' +export * from '../groupTypes.js' diff --git a/src/facade/types.ts b/src/facade/types.ts new file mode 100644 index 0000000..b081732 --- /dev/null +++ b/src/facade/types.ts @@ -0,0 +1,147 @@ +import type { CryptoProvider, HashID } from '../cryptoTypes.js' +import type { Elt, Group, Scalar, GroupID } from '../groupTypes.js' +import type { MODE, SUITE } from '../consts.js' + +export type SuiteID = (typeof SUITE)[keyof typeof SUITE] +export type ModeOprf = typeof MODE.OPRF +export type ModePoprf = typeof MODE.POPRF +export type ModeVoprf = typeof MODE.VOPRF +export type ModeID = ModeOprf | ModeVoprf | ModePoprf + +export type HasSerialize = { serialize(): T } +export type Serialized> = T extends HasSerialize + ? R + : never + +export interface Modal { + readonly mode: M + readonly suite: SuiteID +} + +export interface UsesCrypto { + readonly crypto: CryptoProvider + readonly group: Group +} + +export type DLEQProof = { c: Scalar; s: Scalar; serialize(): Uint8Array } +// OPRF protocol only specifies encodings for these elements +export type OPRFElement = Elt | Scalar | DLEQProof + +// So we only encode elements/bytes, and leave packet encoding to application layer +export interface EvaluationRequest { + readonly blinded: Array> +} + +export interface BaseEvaluation { + readonly mode: ModeID + readonly evaluated: Array> +} + +export interface VerifiableEvaluation extends BaseEvaluation { + readonly proof: Serialized +} + +export interface FinalizeData { + readonly inputs: Array + readonly blinds: Array + readonly evalReq: { + readonly blinded: Array // Keep wet elements + } +} + +export interface KeyPair { + readonly privateKey: Uint8Array + readonly publicKey: Uint8Array +} + +export interface KeySizes { + readonly privateKey: number + readonly publicKey: number +} + +export interface KeyManager extends Modal, UsesCrypto { + sizes(): KeySizes + + validatePrivate(privateKey: Uint8Array): boolean + + validatePublic(publicKey: Uint8Array): boolean + + randomPrivate(): Promise + + derivePrivate(seed: Uint8Array, info: Uint8Array): Promise + + generatePublic(privateKey: Uint8Array): Uint8Array + + generatePair(): Promise + + derivePair(seed: Uint8Array, info: Uint8Array): Promise +} + +// Using the tuple labels `info?:` gives us a nice IDE experience +type Info = M extends ModePoprf ? [info?: Uint8Array] : [] +type Evaluation = M extends ModeOprf ? BaseEvaluation : VerifiableEvaluation +type PublicKey = M extends ModeOprf ? [] : [publicKey: Uint8Array] + +export interface OprfBase extends Modal, UsesCrypto {} + +export interface Client extends OprfBase { + spyHandle: { + blinds: { + randomBlinder(): Promise + } + } + + blind(inputs: Uint8Array[]): Promise<[FinalizeData, EvaluationRequest]> + + finalize( + finData: FinalizeData, + evaluation: Omit, 'mode'>, + ...info: Info + ): Promise> +} + +export interface Server extends OprfBase { + spyHandle: { + dleqProver: { + randomScalar(): Promise + } + } + + blindEvaluate(req: EvaluationRequest, ...info: Info): Promise> + + verifyFinalize(input: Uint8Array, output: Uint8Array, ...info: Info): Promise +} + +export interface ModeParams extends Modal { + group: GroupID + hash: HashID + sizes: { + elt: number + scalar: number + proof: number + output: number + } +} + +export interface Mode extends Modal, UsesCrypto { + params: ModeParams + + readonly keys: KeyManager + + makeServer(privateKey: Uint8Array): Server + + makeClient(...publicKey: PublicKey): Client +} + +export interface MakeModeParams extends Modal {} + +export interface OprfApi { + readonly Suite: typeof SUITE + readonly Mode: typeof MODE + + readonly crypto: CryptoProvider + + withConfig(config: { crypto: CryptoProvider }): OprfApi + + makeMode(params: MakeModeParams): Mode +} diff --git a/src/groupSjcl.ts b/src/groupSjcl.ts index 95cc124..4c19823 100644 --- a/src/groupSjcl.ts +++ b/src/groupSjcl.ts @@ -13,8 +13,9 @@ import { type GroupCons, type GroupID, type Scalar, - Groups, - errBadGroup + errBadGroup, + type GroupCache, + GROUP } from './groupTypes.js' function hashParams(hash: string): { @@ -76,14 +77,14 @@ async function expandXMD( function getCurve(gid: GroupID): sjcl.ecc.curve { switch (gid) { - case Groups.P256: + case GROUP.P256: return sjcl.ecc.curves.c256 - case Groups.P384: + case GROUP.P384: return sjcl.ecc.curves.c384 - case Groups.P521: + case GROUP.P521: return sjcl.ecc.curves.c521 - case Groups.DECAF448: - case Groups.RISTRETTO255: + case GROUP.DECAF448: + case GROUP.RISTRETTO255: throw new Error('group: non-supported ciphersuite') default: throw errBadGroup(gid) @@ -190,19 +191,19 @@ function getSSWUParams(gid: GroupID): SSWUParams { let Z let c2 switch (gid) { - case Groups.P256: + case GROUP.P256: Z = -10 // c2 = sqrt(-Z) in GF(p). c2 = '0x25ac71c31e27646736870398ae7f554d8472e008b3aa2a49d332cbd81bcc3b80' break - case Groups.P384: + case GROUP.P384: Z = -12 // c2 = sqrt(-Z) in GF(p). c2 = '0x2accb4a656b0249c71f0500e83da2fdd7f98e383d68b53871f872fcb9ccb80c53c0de1f8a80f7e1914e2ec69f5a626b3' break - case Groups.P521: + case GROUP.P521: Z = -4 // c2 = sqrt(-Z) in GF(p). c2 = '0x2' @@ -226,11 +227,11 @@ interface HashParams { function getHashParams(gid: GroupID): HashParams { switch (gid) { - case Groups.P256: + case GROUP.P256: return { hash: 'SHA-256', L: 48 } - case Groups.P384: + case GROUP.P384: return { hash: 'SHA-384', L: 72 } - case Groups.P521: + case GROUP.P521: return { hash: 'SHA-512', L: 98 } default: throw errBadGroup(gid) @@ -477,9 +478,11 @@ class EltSj implements Elt { } class GroupSj implements Group { - static readonly supportedGroups: GroupID[] = [Groups.P256, Groups.P384, Groups.P521] - static fromID(gid: GroupID) { - return new this(gid) + static readonly supportedGroups: GroupID[] = [GROUP.P256, GROUP.P384, GROUP.P521] + static readonly #cache: GroupCache = {} + + static get(gid: GroupID) { + return (this.#cache[`${gid}`] ??= new this(gid)) } readonly id: GroupID @@ -487,13 +490,13 @@ class GroupSj implements Group { constructor(gid: GroupID) { switch (gid) { - case Groups.P256: + case GROUP.P256: this.size = 32 break - case Groups.P384: + case GROUP.P384: this.size = 48 break - case Groups.P521: + case GROUP.P521: this.size = 66 break default: diff --git a/src/groupTypes.ts b/src/groupTypes.ts index c38d43c..da7dc35 100644 --- a/src/groupTypes.ts +++ b/src/groupTypes.ts @@ -3,7 +3,7 @@ // Licensed under the BSD-3-Clause license found in the LICENSE file or // at https://opensource.org/licenses/BSD-3-Clause -export const Groups = { +export const GROUP = { // P256_XMD:SHA-256_SSWU_RO_ P256: 'P-256', // P384_XMD:SHA-384_SSWU_RO_ @@ -16,7 +16,7 @@ export const Groups = { DECAF448: 'decaf448' } as const -export type GroupID = (typeof Groups)[keyof typeof Groups] +export type GroupID = (typeof GROUP)[keyof typeof GROUP] export function errBadGroup(X: string) { return new Error(`group: bad group name ${X}.`) @@ -98,6 +98,8 @@ export interface Group extends SerializationHelpers { } export interface GroupCons { - fromID(id: GroupID): Group + get(id: GroupID): Group supportedGroups: Array } + +export type GroupCache = Partial> diff --git a/src/keys.ts b/src/keys.ts index 5cdd8de..dd775df 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -3,37 +3,51 @@ // Licensed under the BSD-3-Clause license found in the LICENSE file or // at https://opensource.org/licenses/BSD-3-Clause -import { type ModeID, type SuiteID, Oprf } from './oprf.js' +import { type ModeID, Oprf, type SuiteID } from './oprf.js' import { joinAll, toU16LenPrefix } from './util.js' import { type Scalar } from './groupTypes.js' +import { type CryptoProviderArg, getSuiteGroup } from './cryptoImpl.js' -export function getKeySizes(id: SuiteID): { Nsk: number; Npk: number } { - const gg = Oprf.getGroup(id) +export function getKeySizes(id: SuiteID, ...arg: CryptoProviderArg): { Nsk: number; Npk: number } { + const gg = getSuiteGroup(id, arg) return { Nsk: gg.scalarSize(), Npk: gg.eltSize(true) } } -export function validatePrivateKey(id: SuiteID, privateKey: Uint8Array): boolean { +export function validatePrivateKey( + id: SuiteID, + privateKey: Uint8Array, + ...arg: CryptoProviderArg +): boolean { try { - const s = Oprf.getGroup(id).desScalar(privateKey) + const gg = getSuiteGroup(id, arg) + const s = gg.desScalar(privateKey) return !s.isZero() } catch (_) { return false } } -export function validatePublicKey(id: SuiteID, publicKey: Uint8Array): boolean { +export function validatePublicKey( + id: SuiteID, + publicKey: Uint8Array, + ...arg: CryptoProviderArg +): boolean { try { - const P = Oprf.getGroup(id).desElt(publicKey) + const gg = getSuiteGroup(id, arg) + const P = gg.desElt(publicKey) return !P.isIdentity() } catch (_) { return false } } -export async function randomPrivateKey(id: SuiteID): Promise { - const gg = Oprf.getGroup(id) +export async function randomPrivateKey( + id: SuiteID, + ...arg: CryptoProviderArg +): Promise { let priv: Scalar do { + const gg = getSuiteGroup(id, arg) priv = await gg.randomScalar() } while (priv.isZero()) @@ -44,9 +58,10 @@ export async function derivePrivateKey( mode: ModeID, id: SuiteID, seed: Uint8Array, - info: Uint8Array + info: Uint8Array, + ...arg: CryptoProviderArg ): Promise { - const gg = Oprf.getGroup(id) + const gg = getSuiteGroup(id, arg) const deriveInput = joinAll([seed, ...toU16LenPrefix(info)]) let counter = 0 let priv: Scalar @@ -63,18 +78,23 @@ export async function derivePrivateKey( return priv.serialize() } -export function generatePublicKey(id: SuiteID, privateKey: Uint8Array): Uint8Array { - const gg = Oprf.getGroup(id) +export function generatePublicKey( + id: SuiteID, + privateKey: Uint8Array, + ...arg: CryptoProviderArg +): Uint8Array { + const gg = getSuiteGroup(id, arg) const priv = gg.desScalar(privateKey) const pub = gg.mulGen(priv) return pub.serialize(true) } export async function generateKeyPair( - id: SuiteID + id: SuiteID, + ...arg: CryptoProviderArg ): Promise<{ privateKey: Uint8Array; publicKey: Uint8Array }> { - const privateKey = await randomPrivateKey(id) - const publicKey = generatePublicKey(id, privateKey) + const privateKey = await randomPrivateKey(id, ...arg) + const publicKey = generatePublicKey(id, privateKey, ...arg) return { privateKey, publicKey } } @@ -82,9 +102,10 @@ export async function deriveKeyPair( mode: ModeID, id: SuiteID, seed: Uint8Array, - info: Uint8Array + info: Uint8Array, + ...arg: CryptoProviderArg ): Promise<{ privateKey: Uint8Array; publicKey: Uint8Array }> { - const privateKey = await derivePrivateKey(mode, id, seed, info) - const publicKey = generatePublicKey(id, privateKey) + const privateKey = await derivePrivateKey(mode, id, seed, info, ...arg) + const publicKey = generatePublicKey(id, privateKey, ...arg) return { privateKey, publicKey } } diff --git a/src/noble/edwards.ts b/src/noble/edwards.ts index 05b1bf1..939ad15 100644 --- a/src/noble/edwards.ts +++ b/src/noble/edwards.ts @@ -17,7 +17,7 @@ export interface MakeEdParamsParams { hash: CHash } -export function makeEdParams({ +export function edwardsCurve({ curve, hashID, scalarHash, diff --git a/src/noble/element.ts b/src/noble/element.ts index 31cbdda..7a75cea 100644 --- a/src/noble/element.ts +++ b/src/noble/element.ts @@ -5,9 +5,10 @@ import type { ProjPointType } from '@noble/curves/abstract/weierstrass' import type { Elt } from '../groupTypes.js' -import { ScalarNb } from './scalar.js' import type { Point, PointConstructor } from './types.js' import type { GroupNb } from './group.js' +import type { ScalarNb } from './scalar.js' + import { compat, errDeserialization } from '../util.js' export class EltNb implements Elt { diff --git a/src/noble/group.ts b/src/noble/group.ts index 25f7716..11d563b 100644 --- a/src/noble/group.ts +++ b/src/noble/group.ts @@ -5,7 +5,13 @@ import { bytesToNumberBE, bytesToNumberLE } from '@noble/curves/abstract/utils' -import { type Deserializer, type Group, type GroupID, Groups } from '../groupTypes.js' +import { + type Deserializer, + GROUP, + type Group, + type GroupCache, + type GroupID +} from '../groupTypes.js' import type { GroupParams } from './types.js' import { ScalarNb } from './scalar.js' import { EltNb } from './element.js' @@ -13,15 +19,17 @@ import { getParams } from './params.js' export class GroupNb implements Group { static readonly supportedGroups: GroupID[] = [ - Groups.RISTRETTO255, - Groups.DECAF448, - Groups.P256, - Groups.P384, - Groups.P521 + GROUP.RISTRETTO255, + GROUP.DECAF448, + GROUP.P256, + GROUP.P384, + GROUP.P521 ] - static fromID(gid: GroupID) { - return new this(gid) + static readonly #cache: GroupCache = {} + + static get(gid: GroupID) { + return (this.#cache[`${gid}`] ??= new this(gid)) } public readonly params: GroupParams diff --git a/src/noble/params.ts b/src/noble/params.ts index 34c156c..533317c 100644 --- a/src/noble/params.ts +++ b/src/noble/params.ts @@ -2,43 +2,43 @@ // Copyright (c) 2021 Cloudflare, Inc. // Licensed under the BSD-3-Clause license found in the LICENSE file or // at https://opensource.org/licenses/BSD-3-Clause - import * as p256 from '@noble/curves/p256' import * as p384 from '@noble/curves/p384' import * as p521 from '@noble/curves/p521' import * as ed25519 from '@noble/curves/ed25519' -import { hashToRistretto255 } from '@noble/curves/ed25519' import * as ed448 from '@noble/curves/ed448' +import { hashToRistretto255 } from '@noble/curves/ed25519' import { hashToDecaf448 } from '@noble/curves/ed448' +import { sha512 } from '@noble/hashes/sha512' -import { errBadGroup, type GroupID, Groups } from '../groupTypes.js' +import { errBadGroup, GROUP, type GroupID } from '../groupTypes.js' import type { GroupParams } from './types.js' -import { makeShortParams } from './short.js' -import { makeEdParams } from './edwards.js' + +import { shortCurve } from './short.js' +import { edwardsCurve } from './edwards.js' import { shake256_512 } from './hashes.js' -import { sha512 } from '@noble/hashes/sha512' const GROUPS: Record = { - [Groups.P256]: makeShortParams({ + [GROUP.P256]: shortCurve({ curve: p256.p256, hashID: 'SHA-256', elementHash: p256.hashToCurve, securityBits: 128 }), - [Groups.P384]: makeShortParams({ + [GROUP.P384]: shortCurve({ curve: p384.p384, hashID: 'SHA-384', elementHash: p384.hashToCurve, securityBits: 192 }), - [Groups.P521]: makeShortParams({ + [GROUP.P521]: shortCurve({ curve: p521.p521, hashID: 'SHA-512', elementHash: p521.hashToCurve, securityBits: 256 }), - [Groups.RISTRETTO255]: makeEdParams({ + [GROUP.RISTRETTO255]: edwardsCurve({ curve: ed25519.ed25519, hashID: 'SHA-512', scalarHash: { type: 'xmd' }, @@ -48,7 +48,7 @@ const GROUPS: Record = { }, hash: sha512 }), - [Groups.DECAF448]: makeEdParams({ + [GROUP.DECAF448]: edwardsCurve({ curve: ed448.ed448, hashID: 'SHAKE256', scalarHash: { type: 'xof', k: 448 }, @@ -61,7 +61,6 @@ const GROUPS: Record = { } export function getParams(gid: GroupID) { - if (!Object.values(Groups).includes(gid)) throw errBadGroup(gid) - // eslint-disable-next-line security/detect-object-injection - return GROUPS[gid] + if (!Object.values(GROUP).includes(gid)) throw errBadGroup(gid) + return GROUPS[`${gid}`] } diff --git a/src/noble/scalar.ts b/src/noble/scalar.ts index b6ca0a5..38a3e66 100644 --- a/src/noble/scalar.ts +++ b/src/noble/scalar.ts @@ -13,9 +13,7 @@ import type { Scalar } from '../groupTypes.js' import { checkSize, compat, errDeserialization } from '../util.js' import type { PrimeField } from './types.js' - import type { GroupNb } from './group.js' - export class ScalarNb implements Scalar { private readonly field: PrimeField public readonly k: bigint diff --git a/src/noble/short.ts b/src/noble/short.ts index 47408af..76b64bb 100644 --- a/src/noble/short.ts +++ b/src/noble/short.ts @@ -15,7 +15,7 @@ export interface MakeShortParamsArgs { securityBits: number } -export function makeShortParams({ +export function shortCurve({ hashID, curve, elementHash, diff --git a/src/oprf.ts b/src/oprf.ts index d1f5cbc..8027d5e 100644 --- a/src/oprf.ts +++ b/src/oprf.ts @@ -5,8 +5,8 @@ import { type DLEQParams, DLEQProof } from './dleq.js' import { - Groups, type Elt, + GROUP, type Group, type GroupCons, type GroupID, @@ -22,7 +22,14 @@ import { toU16LenPrefixUint8Array } from './util.js' import type { CryptoProvider, HashID } from './cryptoTypes.js' -import { CryptoSjcl } from './cryptoSjcl.js' +import { LABELS, MODE, SUITE } from './consts.js' +import { + type CryptoProviderArg, + getCrypto, + getSuiteGroup, + getCryptoProvider, + setCryptoProvider +} from './cryptoImpl.js' export type ModeID = (typeof Oprf.Mode)[keyof typeof Oprf.Mode] export type SuiteID = (typeof Oprf.Suite)[keyof typeof Oprf.Suite] @@ -31,18 +38,20 @@ function assertNever(name: string, x: unknown): never { throw new Error(`unexpected ${name} identifier: ${x}`) } -function getOprfParams(id: string): readonly [SuiteID, GroupID, HashID, number] { +export function getOprfParams( + id: string +): readonly [suite: SuiteID, group: GroupID, hash: HashID, size: number] { switch (id) { case Oprf.Suite.P256_SHA256: - return [Oprf.Suite.P256_SHA256, Groups.P256, 'SHA-256', 32] + return [Oprf.Suite.P256_SHA256, GROUP.P256, 'SHA-256', 32] case Oprf.Suite.P384_SHA384: - return [Oprf.Suite.P384_SHA384, Groups.P384, 'SHA-384', 48] + return [Oprf.Suite.P384_SHA384, GROUP.P384, 'SHA-384', 48] case Oprf.Suite.P521_SHA512: - return [Oprf.Suite.P521_SHA512, Groups.P521, 'SHA-512', 64] + return [Oprf.Suite.P521_SHA512, GROUP.P521, 'SHA-512', 64] case Oprf.Suite.RISTRETTO255_SHA512: - return [Oprf.Suite.RISTRETTO255_SHA512, Groups.RISTRETTO255, 'SHA-512', 64] + return [Oprf.Suite.RISTRETTO255_SHA512, GROUP.RISTRETTO255, 'SHA-512', 64] case Oprf.Suite.DECAF448_SHAKE256: - return [Oprf.Suite.DECAF448_SHAKE256, Groups.DECAF448, 'SHAKE256', 64] + return [Oprf.Suite.DECAF448_SHAKE256, GROUP.DECAF448, 'SHAKE256', 64] default: assertNever('Oprf.Suite', id) } @@ -54,34 +63,17 @@ export function getSupportedSuites(g: GroupCons): Array { } export abstract class Oprf { - public static Crypto: CryptoProvider = CryptoSjcl + static set Crypto(provider: CryptoProvider) { + setCryptoProvider(provider) + } - public static get Group(): GroupCons { - return this.Crypto.Group + static get Crypto(): CryptoProvider { + return getCryptoProvider() } - static Mode = { - OPRF: 0, - VOPRF: 1, - POPRF: 2 - } as const - - static Suite = { - P256_SHA256: 'P256-SHA256', - P384_SHA384: 'P384-SHA384', - P521_SHA512: 'P521-SHA512', - RISTRETTO255_SHA512: 'ristretto255-SHA512', - DECAF448_SHAKE256: 'decaf448-SHAKE256' - } as const - - static LABELS = { - Version: 'OPRFV1-', - FinalizeDST: 'Finalize', - HashToGroupDST: 'HashToGroup-', - HashToScalarDST: 'HashToScalar-', - DeriveKeyPairDST: 'DeriveKeyPair', - InfoLabel: 'Info' - } as const + static Mode = MODE + static Suite = SUITE + static LABELS = LABELS private static validateMode(m: ModeID): ModeID { switch (m) { @@ -94,8 +86,8 @@ export abstract class Oprf { } } - static getGroup(suite: SuiteID): Group { - return Oprf.Group.fromID(getOprfParams(suite)[1]) + static getGroup(suite: SuiteID, ...arg: CryptoProviderArg): Group { + return getSuiteGroup(suite, arg) } static getHash(suite: SuiteID): HashID { @@ -117,30 +109,28 @@ export abstract class Oprf { } readonly mode: ModeID - readonly ID: SuiteID - readonly gg: Group + readonly suite: SuiteID readonly hashID: HashID - constructor(mode: ModeID, suite: SuiteID) { + readonly group: Group + readonly crypto: CryptoProvider + + protected constructor(mode: ModeID, suite: SuiteID, ...arg: CryptoProviderArg) { const [ID, gid, hash] = getOprfParams(suite) - this.ID = ID - this.gg = Oprf.Group.fromID(gid) + this.crypto = getCrypto(arg) + this.group = this.crypto.Group.get(gid) + this.suite = ID this.hashID = hash this.mode = Oprf.validateMode(mode) } protected getDLEQParams(): DLEQParams { const EMPTY_DST = '' - return { - group: this.gg, - hashID: this.hashID, - hash: Oprf.Crypto.hash, - dst: this.getDST(EMPTY_DST) - } + return { group: this.group.id, hash: this.hashID, dst: this.getDST(EMPTY_DST) } } protected getDST(name: string): Uint8Array { - return Oprf.getDST(this.mode, this.ID, name) + return Oprf.getDST(this.mode, this.suite, name) } protected async coreFinalize( @@ -159,7 +149,7 @@ export abstract class Oprf { ...toU16LenPrefix(issuedElement), new TextEncoder().encode(Oprf.LABELS.FinalizeDST) ]) - return await Oprf.Crypto.hash(this.hashID, hashInput) + return await this.crypto.hash(this.hashID, hashInput) } protected scalarFromInfo(info: Uint8Array): Promise { @@ -168,7 +158,7 @@ export abstract class Oprf { } const te = new TextEncoder() const framedInfo = joinAll([te.encode(Oprf.LABELS.InfoLabel), ...toU16LenPrefix(info)]) - return this.gg.hashToScalar(framedInfo, this.getDST(Oprf.LABELS.HashToScalarDST)) + return this.group.hashToScalar(framedInfo, this.getDST(Oprf.LABELS.HashToScalarDST)) } } @@ -203,8 +193,9 @@ export class Evaluation { return res } - static deserialize(suite: SuiteID, bytes: Uint8Array): Evaluation { - const group = Oprf.getGroup(suite) + static deserialize(suite: SuiteID, bytes: Uint8Array, ...arg: CryptoProviderArg): Evaluation { + const group = getSuiteGroup(suite, arg) + const { head: evalList, tail } = fromU16LenPrefixDes(group.eltDes, bytes) let proof: DLEQProof | undefined const proofSize = DLEQProof.size(group) @@ -215,7 +206,7 @@ export class Evaluation { break case Oprf.Mode.VOPRF: case Oprf.Mode.POPRF: - proof = DLEQProof.deserialize(group, proofBytes) + proof = DLEQProof.deserialize(group.id, proofBytes, ...arg) break default: assertNever('Oprf.Mode', mode) @@ -235,8 +226,12 @@ export class EvaluationRequest { return this.blinded.every((x, i) => x.isEqual(e.blinded[i as number])) } - static deserialize(suite: SuiteID, bytes: Uint8Array): EvaluationRequest { - const g = Oprf.getGroup(suite) + static deserialize( + suite: SuiteID, + bytes: Uint8Array, + ...arg: CryptoProviderArg + ): EvaluationRequest { + const g = getSuiteGroup(suite, arg) const { head: blindedList } = fromU16LenPrefixDes(g.eltDes, bytes) return new EvaluationRequest(blindedList) } @@ -265,11 +260,11 @@ export class FinalizeData { ) } - static deserialize(suite: SuiteID, bytes: Uint8Array): FinalizeData { - const g = Oprf.getGroup(suite) + static deserialize(suite: SuiteID, bytes: Uint8Array, ...arg: CryptoProviderArg): FinalizeData { + const g = getSuiteGroup(suite, arg) const { head: inputs, tail: t0 } = fromU16LenPrefixUint8Array(bytes) const { head: blinds, tail: t1 } = fromU16LenPrefixDes(g.scalarDes, t0) - const evalReq = EvaluationRequest.deserialize(suite, t1) + const evalReq = EvaluationRequest.deserialize(suite, t1, ...arg) return new FinalizeData(inputs, blinds, evalReq) } } diff --git a/src/server.ts b/src/server.ts index 983a975..def2326 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,14 +7,15 @@ import { DLEQProver } from './dleq.js' import type { Elt, Scalar } from './groupTypes.js' import { Evaluation, EvaluationRequest, type ModeID, Oprf, type SuiteID } from './oprf.js' import { ctEqual, zip } from './util.js' +import type { CryptoProviderArg } from './cryptoImpl.js' class baseServer extends Oprf { + protected prover = new DLEQProver(this.getDLEQParams(), this.crypto) // hook-able protected privateKey: Uint8Array - public supportsWebCryptoOPRF = false - constructor(mode: ModeID, suite: SuiteID, privateKey: Uint8Array) { - super(mode, suite) + constructor(mode: ModeID, suite: SuiteID, privateKey: Uint8Array, ...arg: CryptoProviderArg) { + super(mode, suite, ...arg) this.privateKey = privateKey } @@ -30,7 +31,7 @@ class baseServer extends Oprf { key, { name: 'OPRF', - namedCurve: this.gg.id + namedCurve: this.group.id }, true, ['sign'] @@ -38,16 +39,16 @@ class baseServer extends Oprf { // webcrypto accepts only compressed points. const compressed = blinded.serialize(true) const evalBytes = new Uint8Array(await crypto.subtle.sign('OPRF', crKey, compressed)) - return this.gg.desElt(evalBytes) + return this.group.desElt(evalBytes) } private blindEvaluateGroup(blinded: Elt, key: Uint8Array): Elt { - return blinded.mul(this.gg.desScalar(key)) + return blinded.mul(this.group.desScalar(key)) } protected async secretFromInfo(info: Uint8Array): Promise<[Scalar, Scalar]> { const m = await this.scalarFromInfo(info) - const skS = this.gg.desScalar(this.privateKey) + const skS = this.group.desScalar(this.privateKey) const t = m.add(skS) if (t.isZero()) { throw new Error('inverse of zero') @@ -63,7 +64,7 @@ class baseServer extends Oprf { secret = evalSecret.serialize() } - const P = await this.gg.hashToGroup(input, this.getDST(Oprf.LABELS.HashToGroupDST)) + const P = await this.group.hashToGroup(input, this.getDST(Oprf.LABELS.HashToGroupDST)) if (P.isIdentity()) { throw new Error('InvalidInputError') } @@ -73,8 +74,8 @@ class baseServer extends Oprf { } export class OPRFServer extends baseServer { - constructor(suite: SuiteID, privateKey: Uint8Array) { - super(Oprf.Mode.OPRF, suite, privateKey) + constructor(suite: SuiteID, privateKey: Uint8Array, ...arg: CryptoProviderArg) { + super(Oprf.Mode.OPRF, suite, privateKey, ...arg) } async blindEvaluate(req: EvaluationRequest): Promise { @@ -83,62 +84,68 @@ export class OPRFServer extends baseServer { await Promise.all(req.blinded.map((b) => this.doBlindEvaluation(b, this.privateKey))) ) } + async evaluate(input: Uint8Array): Promise { return this.doEvaluate(input) } + async verifyFinalize(input: Uint8Array, output: Uint8Array): Promise { return ctEqual(output, await this.doEvaluate(input)) } } export class VOPRFServer extends baseServer { - constructor(suite: SuiteID, privateKey: Uint8Array) { - super(Oprf.Mode.VOPRF, suite, privateKey) + constructor(suite: SuiteID, privateKey: Uint8Array, ...arg: CryptoProviderArg) { + super(Oprf.Mode.VOPRF, suite, privateKey, ...arg) } + async blindEvaluate(req: EvaluationRequest): Promise { const evalList = await Promise.all( req.blinded.map((b) => this.doBlindEvaluation(b, this.privateKey)) ) - const prover = new DLEQProver(this.getDLEQParams()) - const skS = this.gg.desScalar(this.privateKey) - const pkS = this.gg.mulGen(skS) - const proof = await prover.prove_batch( + const skS = this.group.desScalar(this.privateKey) + const pkS = this.group.mulGen(skS) + const proof = await this.prover.prove_batch( skS, - [this.gg.generator(), pkS], + [this.group.generator(), pkS], zip(req.blinded, evalList) ) return new Evaluation(this.mode, evalList, proof) } + async evaluate(input: Uint8Array): Promise { return this.doEvaluate(input) } + async verifyFinalize(input: Uint8Array, output: Uint8Array): Promise { return ctEqual(output, await this.doEvaluate(input)) } } export class POPRFServer extends baseServer { - constructor(suite: SuiteID, privateKey: Uint8Array) { - super(Oprf.Mode.POPRF, suite, privateKey) + constructor(suite: SuiteID, privateKey: Uint8Array, ...arg: CryptoProviderArg) { + super(Oprf.Mode.POPRF, suite, privateKey, ...arg) } + async blindEvaluate(req: EvaluationRequest, info = new Uint8Array(0)): Promise { const [keyProof, evalSecret] = await this.secretFromInfo(info) const secret = evalSecret.serialize() const evalList = await Promise.all( req.blinded.map((b) => this.doBlindEvaluation(b, secret)) ) - const prover = new DLEQProver(this.getDLEQParams()) - const kG = this.gg.mulGen(keyProof) - const proof = await prover.prove_batch( + const kG = this.group.mulGen(keyProof) + const proof = await this.prover.prove_batch( keyProof, - [this.gg.generator(), kG], + [this.group.generator(), kG], zip(evalList, req.blinded) ) return new Evaluation(this.mode, evalList, proof) } + async evaluate(input: Uint8Array, info = new Uint8Array(0)): Promise { return this.doEvaluate(input, info) } + async verifyFinalize( input: Uint8Array, output: Uint8Array, diff --git a/src/sjcl/index.js b/src/sjcl/index.js index b59be1e..db21024 100644 --- a/src/sjcl/index.js +++ b/src/sjcl/index.js @@ -3599,12 +3599,12 @@ sjcl.codec.arrayBuffer = { } }; -if(typeof module !== 'undefined' && module.exports){ - module.exports = sjcl; -} -if (typeof define === "function") { - define([], function () { - return sjcl; - }); -} +// if(typeof module !== 'undefined' && module.exports){ +// module.exports = sjcl; +// } +// if (typeof define === "function") { +// define([], function () { +// return sjcl; +// }); +// } export default sjcl; diff --git a/test/describeCryptoTests.ts b/test/describeCryptoTests.ts new file mode 100644 index 0000000..52c3699 --- /dev/null +++ b/test/describeCryptoTests.ts @@ -0,0 +1,30 @@ +import { CryptoNoble } from '../src/cryptoNoble.js' +import { + type CryptoProvider, + getOprfParams, + getSupportedSuites, + type GroupID +} from '../src/index.js' +import { CryptoSjcl } from '../src/cryptoSjcl.js' + +const cryptoProviderMatch = process.env.CRYPTO_PROVIDER +const allProviders = [CryptoNoble, CryptoSjcl] +export const testProviders = allProviders + .filter((provider) => !cryptoProviderMatch || provider.id === cryptoProviderMatch) + .map((p) => [p.id, p] as const) + +export function describeCryptoTests( + declare: (args: { + provider: CryptoProvider + supportedSuites: Array> + supportedGroups: GroupID[] + }) => void +) { + describe.each(testProviders)(`CryptoProvider({id: '%s'})`, (_, provider) => { + declare({ + provider: provider, + supportedSuites: getSupportedSuites(provider.Group).map(getOprfParams), + supportedGroups: provider.Group.supportedGroups + }) + }) +} diff --git a/test/dleq.test.ts b/test/dleq.test.ts index ffbfa1e..e287223 100644 --- a/test/dleq.test.ts +++ b/test/dleq.test.ts @@ -5,29 +5,27 @@ import { type DLEQParams, - type Elt, - type Scalar, DLEQProof, DLEQProver, - Oprf, - DLEQVerifier + DLEQVerifier, + type Elt, + type Scalar } from '../src/index.js' -import { describeGroupTests } from './describeGroupTests.js' +import { describeCryptoTests } from './describeCryptoTests.js' import { serdeClass } from './util.js' -describeGroupTests((g) => { - describe.each(g.supportedGroups)('DLEQ', (id) => { +describeCryptoTests(({ provider, supportedGroups }) => { + describe.each(supportedGroups)('DLEQ', (id) => { const groupName = id - const gg = Oprf.Crypto.Group.fromID(id) + const group = provider.Group.get(id) const te = new TextEncoder() const params: DLEQParams = { - group: gg, - hash: Oprf.Crypto.hash, - hashID: 'SHA-256', + group: id, + hash: 'SHA-256', dst: te.encode('domain-sep') } - const Peggy = new DLEQProver(params) - const Victor = new DLEQVerifier(params) + const Peggy = new DLEQProver(params, provider) + const Victor = new DLEQVerifier(params, provider) let k: Scalar let P: Elt @@ -40,16 +38,16 @@ describeGroupTests((g) => { describe.each([...Array(5).keys()])(`${groupName}`, (i: number) => { beforeAll(async () => { - k = await gg.randomScalar() - P = gg.mulGen(await gg.randomScalar()) + k = await group.randomScalar() + P = group.mulGen(await group.randomScalar()) kP = P.mul(k) - Q = gg.mulGen(await gg.randomScalar()) + Q = group.mulGen(await group.randomScalar()) kQ = Q.mul(k) proof = await Peggy.prove(k, [P, kP], [Q, kQ]) list = new Array<[Elt, Elt]>() for (let l = 0; l < 3; l++) { - const R = gg.mulGen(await gg.randomScalar()) + const R = group.mulGen(await group.randomScalar()) const kR = R.mul(k) list.push([R, kR]) } @@ -73,14 +71,14 @@ describeGroupTests((g) => { }) it(`bad-key/${i}`, async () => { - const badKey = await gg.randomScalar() + const badKey = await group.randomScalar() const badProof: DLEQProof = await Peggy.prove(badKey, [P, kP], [Q, kQ]) expect(await Victor.verify([P, kP], [Q, kQ], badProof)).toBe(false) }) it(`serde/${i}`, async () => { - expect(serdeClass(DLEQProof, proof, gg)).toBe(true) - expect(serdeClass(DLEQProof, proofBatched, gg)).toBe(true) + expect(serdeClass(DLEQProof, proof, id, provider)).toBe(true) + expect(serdeClass(DLEQProof, proofBatched, id, provider)).toBe(true) }) }) }) diff --git a/test/group.test.ts b/test/group.test.ts index 60c3b1e..aa1a9a7 100644 --- a/test/group.test.ts +++ b/test/group.test.ts @@ -3,12 +3,12 @@ // Licensed under the BSD-3-Clause license found in the LICENSE file or // at https://opensource.org/licenses/BSD-3-Clause -import { describeGroupTests } from './describeGroupTests.js' +import { describeCryptoTests } from './describeCryptoTests.js' import { serdesEquals } from './util.js' -describeGroupTests((Group) => { - describe.each(Group.supportedGroups)('%s', (id) => { - const gg = Group.fromID(id) +describeCryptoTests(({ provider, supportedGroups }) => { + describe.each(supportedGroups)('%s', (id) => { + const gg = provider.Group.get(id) it('serdeElement', async () => { const P = gg.mulGen(await gg.randomScalar()) diff --git a/test/keys.test.ts b/test/keys.test.ts index bb6bc52..9796432 100644 --- a/test/keys.test.ts +++ b/test/keys.test.ts @@ -7,18 +7,17 @@ import { deriveKeyPair, generateKeyPair, getKeySizes, - getSupportedSuites, Oprf, validatePrivateKey, validatePublicKey } from '../src/index.js' -import { describeGroupTests } from './describeGroupTests.js' +import { describeCryptoTests } from './describeCryptoTests.js' -describeGroupTests((g) => { - describe.each(getSupportedSuites(g))('oprf-keys', (id) => { +describeCryptoTests(({ provider, supportedSuites }) => { + describe.each(supportedSuites)('oprf-keys', (id, groupID) => { describe(`${id}`, () => { - const { Nsk, Npk } = getKeySizes(id) - const gg = Oprf.getGroup(id) + const { Nsk, Npk } = getKeySizes(id, provider) + const gg = provider.Group.get(groupID) it('getKeySizes', () => { expect(Nsk).toBe(gg.scalarSize()) @@ -27,40 +26,40 @@ describeGroupTests((g) => { it('zeroPrivateKey', () => { const zeroKeyBytes = new Uint8Array(Nsk) - const ret = validatePrivateKey(id, zeroKeyBytes) + const ret = validatePrivateKey(id, zeroKeyBytes, provider) expect(ret).toBe(false) }) it('badPrivateKey', () => { const bad = new Uint8Array(100) bad.fill(0xff) - const ret = validatePrivateKey(id, bad) + const ret = validatePrivateKey(id, bad, provider) expect(ret).toBe(false) }) it('onesPrivateKey', () => { const onesKeyBytes = new Uint8Array(Nsk).fill(0xff) - const ret = validatePrivateKey(id, onesKeyBytes) + const ret = validatePrivateKey(id, onesKeyBytes, provider) expect(ret).toBe(false) }) it('identityPublicKey', () => { const identityKeyBytes = gg.identity().serialize() - const ret = validatePublicKey(id, identityKeyBytes) + const ret = validatePublicKey(id, identityKeyBytes, provider) expect(ret).toBe(false) }) it('onesPublicKey', () => { const onesKeyBytes = new Uint8Array(Npk).fill(0xff) - const ret = validatePublicKey(id, onesKeyBytes) + const ret = validatePublicKey(id, onesKeyBytes, provider) expect(ret).toBe(false) }) it('generateKeyPair', async () => { for (let i = 0; i < 64; i++) { - const keys = await generateKeyPair(id) // eslint-disable-line no-await-in-loop - const sk = validatePrivateKey(id, keys.privateKey) - const pk = validatePublicKey(id, keys.publicKey) + const keys = await generateKeyPair(id, provider) // eslint-disable-line no-await-in-loop + const sk = validatePrivateKey(id, keys.privateKey, provider) + const pk = validatePublicKey(id, keys.publicKey, provider) expect(sk).toBe(true) expect(pk).toBe(true) } @@ -70,9 +69,9 @@ describeGroupTests((g) => { const info = new TextEncoder().encode('info used for derivation') for (let i = 0; i < 64; i++) { const seed = crypto.getRandomValues(new Uint8Array(Nsk)) - const keys = await deriveKeyPair(Oprf.Mode.OPRF, id, seed, info) // eslint-disable-line no-await-in-loop - const sk = validatePrivateKey(id, keys.privateKey) - const pk = validatePublicKey(id, keys.publicKey) + const keys = await deriveKeyPair(Oprf.Mode.OPRF, id, seed, info, provider) // eslint-disable-line no-await-in-loop + const sk = validatePrivateKey(id, keys.privateKey, provider) + const pk = validatePublicKey(id, keys.publicKey, provider) expect(sk).toBe(true) expect(pk).toBe(true) } diff --git a/test/oprf.test.ts b/test/oprf.test.ts index 9223c72..5f1647e 100644 --- a/test/oprf.test.ts +++ b/test/oprf.test.ts @@ -4,59 +4,60 @@ // at https://opensource.org/licenses/BSD-3-Clause import { + type CryptoProvider, Evaluation, EvaluationRequest, FinalizeData, generatePublicKey, - getSupportedSuites, Oprf, OPRFClient, OPRFServer, POPRFClient, POPRFServer, randomPrivateKey, + type SuiteID, VOPRFClient, - VOPRFServer, - type SuiteID + VOPRFServer } from '../src/index.js' +import { describeCryptoTests } from './describeCryptoTests.js' -import { describeGroupTests } from './describeGroupTests.js' import { serdeClass } from './util.js' async function testBadProof( - id: SuiteID, client: OPRFClient, finData: FinalizeData, - evaluation: Evaluation + evaluation: Evaluation, + crypto: CryptoProvider, + suiteID: SuiteID ) { - const badEval = Evaluation.deserialize(id, evaluation.serialize()) + const badEval = Evaluation.deserialize(suiteID, evaluation.serialize(), crypto) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Object.assign(badEval.proof!, { s: evaluation.proof!.c }) await expect(client.finalize(finData, badEval)).rejects.toThrow(/proof failed/) } -describeGroupTests((g) => { +describeCryptoTests(({ provider, supportedSuites }) => { describe.each(Object.entries(Oprf.Mode))('protocol', (modeName, mode) => { - describe.each(getSupportedSuites(g))(`${modeName}`, (id) => { + describe.each(supportedSuites)(`${modeName}`, (id) => { let server: OPRFServer | VOPRFServer | POPRFServer let client: OPRFClient | VOPRFClient | POPRFClient beforeAll(async () => { - const privateKey = await randomPrivateKey(id) - const publicKey = generatePublicKey(id, privateKey) + const privateKey = await randomPrivateKey(id, provider) + const publicKey = generatePublicKey(id, privateKey, provider) switch (mode) { case Oprf.Mode.OPRF: - server = new OPRFServer(id, privateKey) - client = new OPRFClient(id) + server = new OPRFServer(id, privateKey, provider) + client = new OPRFClient(id, provider) break case Oprf.Mode.VOPRF: - server = new VOPRFServer(id, privateKey) - client = new VOPRFClient(id, publicKey) + server = new VOPRFServer(id, privateKey, provider) + client = new VOPRFClient(id, publicKey, provider) break case Oprf.Mode.POPRF: - server = new POPRFServer(id, privateKey) - client = new POPRFClient(id, publicKey) + server = new POPRFServer(id, privateKey, provider) + client = new POPRFClient(id, publicKey, provider) break } }) @@ -83,7 +84,7 @@ describeGroupTests((g) => { expect(output).toHaveLength(Oprf.getOprfSize(id)) if (evaluation.proof) { - await testBadProof(id, client, finData, evaluation) + await testBadProof(client, finData, evaluation, provider, id) } const serverOutput = await server.evaluate(input) @@ -92,9 +93,9 @@ describeGroupTests((g) => { const success = await server.verifyFinalize(input, output) expect(success).toBe(true) - expect(serdeClass(FinalizeData, finData, id)).toBe(true) - expect(serdeClass(EvaluationRequest, evalReq, id)).toBe(true) - expect(serdeClass(Evaluation, evaluation, id)).toBe(true) + expect(serdeClass(FinalizeData, finData, id, provider)).toBe(true) + expect(serdeClass(EvaluationRequest, evalReq, id, provider)).toBe(true) + expect(serdeClass(Evaluation, evaluation, id, provider)).toBe(true) }) }) }) diff --git a/test/server.test.ts b/test/server.test.ts index f65bf8a..4f3dbf3 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -5,15 +5,8 @@ import { jest } from '@jest/globals' -import { - getSupportedSuites, - type GroupID, - Oprf, - OPRFClient, - OPRFServer, - randomPrivateKey -} from '../src/index.js' -import { describeGroupTests } from './describeGroupTests.js' +import { type GroupID, OPRFClient, OPRFServer, randomPrivateKey } from '../src/index.js' +import { describeCryptoTests } from './describeCryptoTests.js' const { sign, importKey } = crypto.subtle @@ -37,23 +30,23 @@ function mockImportKey(...x: Parameters): ReturnType): ReturnType { - const [algorithm, key, data] = x - if (algorithm === 'OPRF') { - const algorithmName = (key.algorithm as EcdsaParams).name - const g = Oprf.Group.fromID(algorithmName as GroupID) - const P = g.desElt(new Uint8Array(data as ArrayBuffer)) - const serSk = new Uint8Array((key as CryptoKeyWithBuffer).keyData) - const sk = g.desScalar(serSk) - const Z = P.mul(sk) - const serZ = Z.serialize() - return Promise.resolve(serZ.buffer as ArrayBuffer) +describeCryptoTests(({ provider, supportedSuites }) => { + function mockSign(...x: Parameters): ReturnType { + const [algorithm, key, data] = x + if (algorithm === 'OPRF') { + const algorithmName = (key.algorithm as EcdsaParams).name + const g = provider.Group.get(algorithmName as GroupID) + const P = g.desElt(new Uint8Array(data as ArrayBuffer)) + const serSk = new Uint8Array((key as CryptoKeyWithBuffer).keyData) + const sk = g.desScalar(serSk) + const Z = P.mul(sk) + const serZ = Z.serialize() + return Promise.resolve(serZ.buffer as ArrayBuffer) + } + throw new Error('bad algorithm') } - throw new Error('bad algorithm') -} -describeGroupTests((g) => { - describe.each(getSupportedSuites(g))('supportsWebCrypto', (id) => { + describe.each(supportedSuites)('supportsWebCrypto', (id) => { beforeAll(() => { jest.spyOn(crypto.subtle, 'importKey').mockImplementation(mockImportKey) jest.spyOn(crypto.subtle, 'sign').mockImplementation(mockSign) @@ -61,9 +54,9 @@ describeGroupTests((g) => { it(`${id}`, async () => { const te = new TextEncoder() - const privateKey = await randomPrivateKey(id) - const server = new OPRFServer(id, privateKey) - const client = new OPRFClient(id) + const privateKey = await randomPrivateKey(id, provider) + const server = new OPRFServer(id, privateKey, provider) + const client = new OPRFClient(id, provider) const input = te.encode('This is the client input') const [, reqEval] = await client.blind([input]) diff --git a/test/util.ts b/test/util.ts index 755bfe6..fa1315a 100644 --- a/test/util.ts +++ b/test/util.ts @@ -3,18 +3,20 @@ // Licensed under the BSD-3-Clause license found in the LICENSE file or // at https://opensource.org/licenses/BSD-3-Clause +import type { CryptoProviderArg } from '../src/cryptoImpl.js' + export function serdeClass< U, K extends { - deserialize: (u: U, b: Uint8Array) => T + deserialize: (u: U, b: Uint8Array, ...arg: CryptoProviderArg) => T }, T extends { serialize: () => Uint8Array isEqual: (t: T) => boolean } ->(k: K, t: T, u: U): boolean { +>(k: K, t: T, u: U, ...arg: CryptoProviderArg): boolean { const ser = t.serialize() - const deser = k.deserialize(u, ser) + const deser = k.deserialize(u, ser, ...arg) return t.isEqual(deser) } diff --git a/test/vectors.test.ts b/test/vectors.test.ts index adf8967..a84e6ef 100644 --- a/test/vectors.test.ts +++ b/test/vectors.test.ts @@ -6,7 +6,6 @@ import { derivePrivateKey, generatePublicKey, - getSupportedSuites, type ModeID, Oprf, OPRFClient, @@ -15,10 +14,9 @@ import { POPRFServer, type SuiteID, VOPRFClient, - VOPRFServer, - DLEQProver + VOPRFServer } from '../src/index.js' -import { describeGroupTests } from './describeGroupTests.js' +import { describeCryptoTests } from './describeCryptoTests.js' // Test vectors taken from reference implementation at https://github.com/cfrg/draft-irtf-cfrg-voprf import allVectors from './testdata/allVectors_v20.json' @@ -45,36 +43,6 @@ function toHexListClass(x: { serialize(): Uint8Array }[]): string { return toHexListUint8Array(x.map((xi) => xi.serialize())) } -class wrapPOPRFServer extends POPRFServer { - info!: Uint8Array - - blindEvaluate( - ...r: Parameters - ): ReturnType { - return super.blindEvaluate(r[0], this.info) - } - - async evaluate(input: Uint8Array): Promise { - return super.evaluate(input, this.info) - } - - async verifyFinalize(input: Uint8Array, output: Uint8Array): Promise { - return super.verifyFinalize(input, output, this.info) - } -} - -class wrapPOPRFClient extends POPRFClient { - info!: Uint8Array - - blind(inputs: Uint8Array[]): ReturnType { - return super.blind(inputs) - } - - finalize(...r: Parameters): ReturnType { - return super.finalize(...r, this.info) - } -} - type SingleVector = { Batch: number Blind: string @@ -99,42 +67,43 @@ type TestVector = { skSm: string vectors: SingleVector[] } - -describeGroupTests((g) => { +describeCryptoTests(({ provider, supportedSuites: supported }) => { // Test vectors from https://datatracker.ietf.org/doc/draft-irtf-cfrg-voprf // https://tools.ietf.org/html/draft-irtf-cfrg-voprf-11 describe.each(allVectors)('test-vectors', (testVector: TestVector) => { const mode = testVector.mode as ModeID const txtMode = Object.entries(Oprf.Mode)[mode as number][0] - const supported = getSupportedSuites(g) const id = testVector.identifier as SuiteID - const describeOrSkip = supported.includes(id) ? describe : describe.skip + const isSupported = supported.find((v) => v[0] === id) + const describeOrSkip = isSupported ? describe : describe.skip describeOrSkip(`${txtMode}, ${id}`, () => { + const suiteParams = isSupported! + let skSm: Uint8Array - let server: OPRFServer | VOPRFServer | wrapPOPRFServer - let client: OPRFClient | VOPRFClient | wrapPOPRFClient + let server: OPRFServer | VOPRFServer | POPRFServer + let client: OPRFClient | VOPRFClient | POPRFClient beforeAll(async () => { const seed = fromHex(testVector.seed) const keyInfo = fromHex(testVector.keyInfo) - skSm = await derivePrivateKey(mode, id, seed, keyInfo) - const pkSm = generatePublicKey(id, skSm) + skSm = await derivePrivateKey(mode, id, seed, keyInfo, provider) + const pkSm = generatePublicKey(id, skSm, provider) switch (mode) { case Oprf.Mode.OPRF: - server = new OPRFServer(id, skSm) - client = new OPRFClient(id) + server = new OPRFServer(id, skSm, provider) + client = new OPRFClient(id, provider) break case Oprf.Mode.VOPRF: - server = new VOPRFServer(id, skSm) - client = new VOPRFClient(id, pkSm) + server = new VOPRFServer(id, skSm, provider) + client = new VOPRFClient(id, pkSm, provider) break case Oprf.Mode.POPRF: - server = new wrapPOPRFServer(id, skSm) - client = new wrapPOPRFClient(id, pkSm) + server = new POPRFServer(id, skSm, provider) + client = new POPRFClient(id, pkSm, provider) break } }) @@ -145,7 +114,7 @@ describeGroupTests((g) => { describe.each(testVector.vectors)('vec$#', (vi: SingleVector) => { it('protocol', async () => { - const group = Oprf.getGroup(id) + const group = provider.Group.get(suiteParams[1]) // Creates a mock for randomBlinder method to // inject the blind value given by the test vector. @@ -160,15 +129,14 @@ describeGroupTests((g) => { // Creates a mock for DLEQProver.randomScalar method to // inject the random value used to generate a DLEQProof. if (vi.Proof) { - jest.spyOn(DLEQProver.prototype, 'randomScalar').mockResolvedValueOnce( + jest.spyOn(server['prover'], 'randomScalar').mockResolvedValueOnce( group.desScalar(fromHex(vi.Proof.r)) ) } - if (testVector.mode === Oprf.Mode.POPRF && vi.Info) { - const info = fromHex(vi.Info) - ;(server as wrapPOPRFServer).info = info - ;(client as wrapPOPRFClient).info = info + let info: Uint8Array | undefined = undefined + if (testVector.mode === Oprf.Mode.POPRF) { + info = fromHex(vi.Info!) } const input = fromHexList(vi.Input) @@ -176,17 +144,17 @@ describeGroupTests((g) => { expect(toHexListClass(finData.blinds)).toEqual(vi.Blind) expect(toHexListClass(evalReq.blinded)).toEqual(vi.BlindedElement) - const ev = await server.blindEvaluate(evalReq) + const ev = await server.blindEvaluate(evalReq, info) expect(toHexListClass(ev.evaluated)).toEqual(vi.EvaluationElement) expect(ev.proof && toHexListClass([ev.proof])).toEqual( vi.Proof && vi.Proof.proof ) - const output = await client.finalize(finData, ev) + const output = await client.finalize(finData, ev, info) expect(toHexListUint8Array(output)).toEqual(vi.Output) const serverCheckOutput = zip(input, output).every( - async (inout) => await server.verifyFinalize(inout[0], inout[1]) + async (inout) => await server.verifyFinalize(inout[0], inout[1], info) ) expect(serverCheckOutput).toBe(true) })