@kokuin/crypto-napi 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/lib/binding.d.ts +44 -0
- package/lib/binding.js +72 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +2 -0
- package/lib/provider.d.ts +30 -0
- package/lib/provider.js +198 -0
- package/package.json +46 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Paul Le Cam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kokuin/crypto-napi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Strict asynchronous crypto provider for Node.js, backed by the kokuin Rust core (`crates/kokuin-crypto`) through Node-API.
|
|
4
|
+
It implements the `@kokuin/crypto` `CryptoProvider` contract and falls back to `@kokuin/crypto-node` when no native binary loads.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createNapiCrypto } from '@kokuin/crypto-napi'
|
|
8
|
+
|
|
9
|
+
const crypto = createNapiCrypto()
|
|
10
|
+
crypto.backend // 'napi', or 'node' after a fallback; `crypto.failure` then says why
|
|
11
|
+
const signature = await crypto.sign({ alg: 'EdDSA', privateKey: seed, message })
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
| Call | Route |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| Ed25519 `sign`, Ed25519 `verify`, `blake3` | Native, on the calling thread |
|
|
17
|
+
| ES256 `verify` and ES256 items of `verifyBatch` | `nodeCrypto` (OpenSSL), about three times faster than the Rust P-256 verifier |
|
|
18
|
+
| `verifyBatch` with two or more Ed25519 items | Native on the libuv thread pool; inputs are copied before the Promise returns |
|
|
19
|
+
| One-item `verifyBatch`, `signBatch` up to 32 items, `blake3Batch` up to 1 MiB | Native, on the calling thread, where the pool hop costs more than it saves |
|
|
20
|
+
| Larger `signBatch` and `blake3Batch` | Native on the libuv thread pool |
|
|
21
|
+
|
|
22
|
+
The routes come from the [Node crypto benchmark gate](../../docs/reference/crypto-node-benchmark.md).
|
|
23
|
+
|
|
24
|
+
Measured through the provider on one x86_64 container with Node 22, against strict noble:
|
|
25
|
+
Ed25519 signing is about 18× faster, Ed25519 verification about 25×, and blake3 on 64 KiB about 110×.
|
|
26
|
+
A batch of 32 Ed25519 verifications costs about 113 µs per item and keeps the event loop free.
|
|
27
|
+
|
|
28
|
+
The backend is chosen once, when `createNapiCrypto()` runs. A missing binary, a binary built for another ABI version
|
|
29
|
+
or one that fails to load selects `nodeCrypto`. A native failure after construction rejects; it never falls back.
|
|
30
|
+
|
|
31
|
+
Arguments are validated in JavaScript exactly as `nobleCrypto` validates them, so every backend raises the same errors.
|
|
32
|
+
Every backend accepts and rejects exactly the same signatures: the shared corpus and the pinned upstream vectors run
|
|
33
|
+
against the Rust core, strict noble, `nodeCrypto` and this provider.
|
|
34
|
+
|
|
35
|
+
Prebuilt binaries are not published yet. In this repository, `pnpm --filter @kokuin/crypto-napi run build:native`
|
|
36
|
+
compiles one for the current host into `native/`. Requires Node.js 22 or later.
|
|
37
|
+
|
|
38
|
+
## Named params migration
|
|
39
|
+
|
|
40
|
+
`CryptoProvider.verify` now accepts one `VerifyItem`: `{ alg, signature, message, publicKey }`.
|
|
41
|
+
`sign`, `signBatch`, `blake3` and `blake3Batch` take flat params objects.
|
|
42
|
+
`verifyBatch` retains its single array argument.
|
|
43
|
+
|
|
44
|
+
| Export | Named params type | Fields |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `NapiBinding.sign` | `NapiBindingSignParams` | `seed, message` |
|
|
47
|
+
| `NapiBinding.signBatch` | `Array<NapiBindingSignParams>` | `seeds, messages` |
|
|
48
|
+
| `NapiBinding.blake3` | `NapiBindingBlake3Params` | `input, length` |
|
|
49
|
+
| `NapiBinding.blake3Batch` | `NapiBindingBlake3BatchParams` | `inputs, length` |
|
|
50
|
+
| `NapiBinding.verify` | `NapiVerifyParams` | `es256, signature, message, publicKey` |
|
|
51
|
+
| `NapiBinding.verifyBatch` | `Array<NapiVerifyParams>` | `es256, signatures, messages, publicKeys` |
|
|
52
|
+
|
|
53
|
+
`NapiBinding` accepts named signing, hashing and verification objects. The loader adapts them to the unchanged positional native ABI.
|
|
54
|
+
|
|
55
|
+
These signature and typing changes preserve cryptographic results, error causes, ordering and storage behaviour.
|
|
56
|
+
|
|
57
|
+
| API | Named params type | Previous positional form |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `CryptoProvider.sign` | `CryptoProviderSignParams` | `sign(alg, privateKey, message)` |
|
|
60
|
+
| `CryptoProvider.signBatch` | `CryptoProviderSignBatchParams` | `signBatch(alg, items)` |
|
|
61
|
+
| `CryptoProvider.blake3` | `CryptoProviderBlake3Params` | `blake3(input, dkLen)` |
|
|
62
|
+
| `CryptoProvider.blake3Batch` | `CryptoProviderBlake3BatchParams` | `blake3Batch(inputs, dkLen)` |
|
package/lib/binding.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type NapiVerifyParams = {
|
|
2
|
+
es256: boolean;
|
|
3
|
+
signature: Uint8Array;
|
|
4
|
+
message: Uint8Array;
|
|
5
|
+
publicKey: Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
/** The ABI the provider expects from `crates/kokuin-crypto-napi` (its `ABI_VERSION`). */
|
|
8
|
+
export declare const NAPI_ABI_VERSION = 1;
|
|
9
|
+
export type NapiBindingSignParams = {
|
|
10
|
+
seed: Uint8Array;
|
|
11
|
+
message: Uint8Array;
|
|
12
|
+
};
|
|
13
|
+
export type NapiBindingBlake3Params = {
|
|
14
|
+
input: Uint8Array;
|
|
15
|
+
length: number;
|
|
16
|
+
};
|
|
17
|
+
export type NapiBindingBlake3BatchParams = {
|
|
18
|
+
inputs: Array<Uint8Array>;
|
|
19
|
+
length: number;
|
|
20
|
+
};
|
|
21
|
+
export type NapiBinding = {
|
|
22
|
+
ABI_VERSION: number;
|
|
23
|
+
sign(params: NapiBindingSignParams): Uint8Array;
|
|
24
|
+
verify(params: NapiVerifyParams): boolean;
|
|
25
|
+
blake3(params: NapiBindingBlake3Params): Uint8Array;
|
|
26
|
+
signBatch(items: Array<NapiBindingSignParams>): Promise<Array<Uint8Array>>;
|
|
27
|
+
verifyBatch(items: Array<NapiVerifyParams>): Promise<Array<boolean>>;
|
|
28
|
+
blake3Batch(params: NapiBindingBlake3BatchParams): Promise<Array<Uint8Array>>;
|
|
29
|
+
};
|
|
30
|
+
/** Why no native binding loaded, or `undefined` when one did. */
|
|
31
|
+
export type NapiLoadFailure = {
|
|
32
|
+
reason: 'missing' | 'abi' | 'load';
|
|
33
|
+
path: string;
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
};
|
|
36
|
+
/** The platform suffix of the binary file name, such as `linux-x64-gnu` or `darwin-arm64`. */
|
|
37
|
+
export declare function napiTarget(): string;
|
|
38
|
+
/** Where the binary for this platform is expected: `native/kokuin-crypto-napi.<target>.node`. */
|
|
39
|
+
export declare function napiBindingPath(): string;
|
|
40
|
+
/** Load the native binding once per process. Never throws; reports why it could not load. */
|
|
41
|
+
export declare function loadNapiBinding(): {
|
|
42
|
+
binding: NapiBinding | null;
|
|
43
|
+
failure?: NapiLoadFailure;
|
|
44
|
+
};
|
package/lib/binding.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** The ABI the provider expects from `crates/kokuin-crypto-napi` (its `ABI_VERSION`). */ export const NAPI_ABI_VERSION = 1;
|
|
6
|
+
function adaptBinding(native) {
|
|
7
|
+
return {
|
|
8
|
+
ABI_VERSION: native.ABI_VERSION,
|
|
9
|
+
sign: ({ seed, message })=>native.sign(seed, message),
|
|
10
|
+
// The native ABI takes parallel arrays. Split here so nothing above it can misalign entries.
|
|
11
|
+
signBatch: (items)=>{
|
|
12
|
+
return native.signBatch(items.map((item)=>item.seed), items.map((item)=>item.message));
|
|
13
|
+
},
|
|
14
|
+
blake3: ({ input, length })=>native.blake3(input, length),
|
|
15
|
+
blake3Batch: ({ inputs, length })=>native.blake3Batch(inputs, length),
|
|
16
|
+
verify: ({ es256, signature, message, publicKey })=>{
|
|
17
|
+
return native.verify(es256, signature, message, publicKey);
|
|
18
|
+
},
|
|
19
|
+
verifyBatch: (items)=>{
|
|
20
|
+
return native.verifyBatch(items.map((item)=>item.es256), items.map((item)=>item.signature), items.map((item)=>item.message), items.map((item)=>item.publicKey));
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function libc() {
|
|
25
|
+
const header = process.report?.getReport()?.header;
|
|
26
|
+
return header?.glibcVersionRuntime ? 'gnu' : 'musl';
|
|
27
|
+
}
|
|
28
|
+
/** The platform suffix of the binary file name, such as `linux-x64-gnu` or `darwin-arm64`. */ export function napiTarget() {
|
|
29
|
+
const base = `${process.platform}-${process.arch}`;
|
|
30
|
+
return process.platform === 'linux' ? `${base}-${libc()}` : base;
|
|
31
|
+
}
|
|
32
|
+
/** Where the binary for this platform is expected: `native/kokuin-crypto-napi.<target>.node`. */ export function napiBindingPath() {
|
|
33
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
return join(root, 'native', `kokuin-crypto-napi.${napiTarget()}.node`);
|
|
35
|
+
}
|
|
36
|
+
let cached;
|
|
37
|
+
/** Load the native binding once per process. Never throws; reports why it could not load. */ export function loadNapiBinding() {
|
|
38
|
+
if (cached != null) return cached;
|
|
39
|
+
const path = napiBindingPath();
|
|
40
|
+
if (!existsSync(path)) {
|
|
41
|
+
cached = {
|
|
42
|
+
binding: null,
|
|
43
|
+
failure: {
|
|
44
|
+
reason: 'missing',
|
|
45
|
+
path
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const binding = createRequire(import.meta.url)(path);
|
|
52
|
+
cached = binding.ABI_VERSION === NAPI_ABI_VERSION ? {
|
|
53
|
+
binding: adaptBinding(binding)
|
|
54
|
+
} : {
|
|
55
|
+
binding: null,
|
|
56
|
+
failure: {
|
|
57
|
+
reason: 'abi',
|
|
58
|
+
path
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
cached = {
|
|
63
|
+
binding: null,
|
|
64
|
+
failure: {
|
|
65
|
+
reason: 'load',
|
|
66
|
+
path,
|
|
67
|
+
cause
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return cached;
|
|
72
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { NapiBinding, NapiBindingBlake3BatchParams, NapiBindingBlake3Params, NapiBindingSignParams, NapiLoadFailure, NapiVerifyParams, } from './binding.js';
|
|
2
|
+
export { loadNapiBinding, NAPI_ABI_VERSION, napiBindingPath, napiTarget } from './binding.js';
|
|
3
|
+
export type { NapiCrypto, NapiCryptoOptions } from './provider.js';
|
|
4
|
+
export { BLAKE3_SYNC_BATCH_BYTES, createNapiCrypto, SIGN_SYNC_BATCH_ITEMS } from './provider.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CryptoProvider } from '@kokuin/crypto';
|
|
2
|
+
import { type NapiBinding, type NapiLoadFailure } from './binding.js';
|
|
3
|
+
export type NapiCrypto = CryptoProvider & {
|
|
4
|
+
/** `napi` when the Rust binding loaded, `node` when the provider fell back to `nodeCrypto`. */
|
|
5
|
+
readonly backend: 'napi' | 'node';
|
|
6
|
+
/** Why the binding did not load, when `backend` is `node`. */
|
|
7
|
+
readonly failure?: NapiLoadFailure;
|
|
8
|
+
};
|
|
9
|
+
export type NapiCryptoOptions = {
|
|
10
|
+
/** Use this binding instead of loading one. `null` forces the `nodeCrypto` fallback. */
|
|
11
|
+
binding?: NapiBinding | null;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* blake3 batches with at most this much input in total hash on the calling thread. Below it the
|
|
15
|
+
* thread-pool hop and input copies cost more than the hashing (1 MiB hashes in about 135 µs on
|
|
16
|
+
* x86_64); above it, the pool keeps a large hash off the event loop.
|
|
17
|
+
*/
|
|
18
|
+
export declare const BLAKE3_SYNC_BATCH_BYTES: number;
|
|
19
|
+
/**
|
|
20
|
+
* Ed25519 signing batches of at most this many items sign on the calling thread: about 27 µs per
|
|
21
|
+
* signature at 256 B and 61 µs at 12 KiB on x86_64, against a thread-pool hop that measured
|
|
22
|
+
* slower for every batch the gate covers. Larger batches go to the pool.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SIGN_SYNC_BATCH_ITEMS = 32;
|
|
25
|
+
/**
|
|
26
|
+
* The Rust core through Node-API when this platform's binary loads, otherwise `nodeCrypto`.
|
|
27
|
+
* The choice is made once, here; a native failure after construction rejects instead of
|
|
28
|
+
* falling back.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createNapiCrypto(options?: NapiCryptoOptions): NapiCrypto;
|
package/lib/provider.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { nodeCrypto } from '@kokuin/crypto-node';
|
|
2
|
+
import { loadNapiBinding } from './binding.js';
|
|
3
|
+
function assertBytes(value) {
|
|
4
|
+
if (!(value instanceof Uint8Array) || !(value.buffer instanceof ArrayBuffer)) {
|
|
5
|
+
throw new TypeError('Expected an unshared Uint8Array');
|
|
6
|
+
}
|
|
7
|
+
// Throws TypeError on a detached buffer.
|
|
8
|
+
new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
9
|
+
}
|
|
10
|
+
function assertSigning(alg, seed, message) {
|
|
11
|
+
if (alg !== 'EdDSA') throw new TypeError('Unsupported signing algorithm');
|
|
12
|
+
assertBytes(seed);
|
|
13
|
+
assertBytes(message);
|
|
14
|
+
if (seed.length !== 32) throw new TypeError('Expected a 32-byte Ed25519 seed');
|
|
15
|
+
}
|
|
16
|
+
function assertVerifying({ alg, signature, message, publicKey }) {
|
|
17
|
+
if (alg !== 'EdDSA' && alg !== 'ES256') throw new TypeError('Unsupported verification algorithm');
|
|
18
|
+
for (const bytes of [
|
|
19
|
+
signature,
|
|
20
|
+
message,
|
|
21
|
+
publicKey
|
|
22
|
+
])assertBytes(bytes);
|
|
23
|
+
}
|
|
24
|
+
function assertOutputLength(dkLen) {
|
|
25
|
+
if (!Number.isInteger(dkLen) || dkLen < 1 || dkLen > 65536) throw new RangeError('Invalid blake3 output length');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* blake3 batches with at most this much input in total hash on the calling thread. Below it the
|
|
29
|
+
* thread-pool hop and input copies cost more than the hashing (1 MiB hashes in about 135 µs on
|
|
30
|
+
* x86_64); above it, the pool keeps a large hash off the event loop.
|
|
31
|
+
*/ export const BLAKE3_SYNC_BATCH_BYTES = 1024 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* Ed25519 signing batches of at most this many items sign on the calling thread: about 27 µs per
|
|
34
|
+
* signature at 256 B and 61 µs at 12 KiB on x86_64, against a thread-pool hop that measured
|
|
35
|
+
* slower for every batch the gate covers. Larger batches go to the pool.
|
|
36
|
+
*/ export const SIGN_SYNC_BATCH_ITEMS = 32;
|
|
37
|
+
function assertResultCount(results, count) {
|
|
38
|
+
if (results.length !== count) throw new Error('Invalid native crypto response count');
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Routes, set by the Node crypto benchmark gate (scripts/bench-crypto-node.mjs):
|
|
43
|
+
* - Ed25519 sign, Ed25519 verify and blake3 single calls run natively on the calling thread.
|
|
44
|
+
* - ES256 verification, single or batched, uses `nodeCrypto`: OpenSSL is about three times
|
|
45
|
+
* faster than the Rust P-256 verifier.
|
|
46
|
+
* - Ed25519 verify batches of two or more copy their inputs on the calling thread, then run
|
|
47
|
+
* natively on the libuv pool, keeping about 100 µs per item off the event loop. A one-item
|
|
48
|
+
* batch runs on the calling thread, where it is faster than the pool hop.
|
|
49
|
+
* - Ed25519 sign batches run natively on the calling thread up to `SIGN_SYNC_BATCH_ITEMS` items
|
|
50
|
+
* and on the pool above it.
|
|
51
|
+
* - blake3 batches run natively on the calling thread up to `BLAKE3_SYNC_BATCH_BYTES` of input
|
|
52
|
+
* and on the pool above it.
|
|
53
|
+
*
|
|
54
|
+
* Arguments are validated in JavaScript exactly as `nobleCrypto` validates them, so every
|
|
55
|
+
* backend raises the same error types. Every method consumes its inputs before its Promise
|
|
56
|
+
* returns.
|
|
57
|
+
*/ function napiProvider(binding) {
|
|
58
|
+
return {
|
|
59
|
+
async sign ({ alg, privateKey: seed, message }) {
|
|
60
|
+
assertSigning(alg, seed, message);
|
|
61
|
+
return binding.sign({
|
|
62
|
+
seed,
|
|
63
|
+
message
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
async signBatch ({ alg, items }) {
|
|
67
|
+
if (alg !== 'EdDSA') throw new TypeError('Unsupported signing algorithm');
|
|
68
|
+
for (const item of items)assertSigning(alg, item.privateKey, item.message);
|
|
69
|
+
if (items.length === 0) return [];
|
|
70
|
+
// Signing is cheap enough that the pool hop costs more than it saves for ordinary batches,
|
|
71
|
+
// and the calling-thread route still takes half nodeCrypto's main-thread time.
|
|
72
|
+
if (items.length <= SIGN_SYNC_BATCH_ITEMS) {
|
|
73
|
+
return items.map((item)=>binding.sign({
|
|
74
|
+
seed: item.privateKey,
|
|
75
|
+
message: item.message
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
// Capture the count before suspending: the caller may change the array afterwards.
|
|
79
|
+
const count = items.length;
|
|
80
|
+
const results = await binding.signBatch(items.map((item)=>({
|
|
81
|
+
seed: item.privateKey,
|
|
82
|
+
message: item.message
|
|
83
|
+
})));
|
|
84
|
+
return assertResultCount(results, count);
|
|
85
|
+
},
|
|
86
|
+
async verify ({ alg, signature, message, publicKey }) {
|
|
87
|
+
assertVerifying({
|
|
88
|
+
alg,
|
|
89
|
+
signature,
|
|
90
|
+
message,
|
|
91
|
+
publicKey
|
|
92
|
+
});
|
|
93
|
+
if (alg === 'ES256') return await nodeCrypto.verify({
|
|
94
|
+
alg,
|
|
95
|
+
signature,
|
|
96
|
+
message,
|
|
97
|
+
publicKey
|
|
98
|
+
});
|
|
99
|
+
return binding.verify({
|
|
100
|
+
es256: false,
|
|
101
|
+
signature,
|
|
102
|
+
message,
|
|
103
|
+
publicKey
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
async verifyBatch (items) {
|
|
107
|
+
for (const item of items){
|
|
108
|
+
assertVerifying({
|
|
109
|
+
alg: item.alg,
|
|
110
|
+
signature: item.signature,
|
|
111
|
+
message: item.message,
|
|
112
|
+
publicKey: item.publicKey
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Capture membership before suspending: the caller may change the array afterwards.
|
|
116
|
+
const algs = items.map((item)=>item.alg);
|
|
117
|
+
const ed25519 = items.filter((item)=>item.alg === 'EdDSA');
|
|
118
|
+
// Start both halves before awaiting either: nodeCrypto consumes its inputs synchronously
|
|
119
|
+
// and the native batch copies them, so caller mutations afterwards change nothing.
|
|
120
|
+
const es256 = items.filter((item)=>item.alg === 'ES256').map((item)=>{
|
|
121
|
+
return nodeCrypto.verify({
|
|
122
|
+
alg: 'ES256',
|
|
123
|
+
signature: item.signature,
|
|
124
|
+
message: item.message,
|
|
125
|
+
publicKey: item.publicKey
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
const native = ed25519.length === 0 ? Promise.resolve([]) : ed25519.length === 1 ? Promise.resolve([
|
|
129
|
+
binding.verify({
|
|
130
|
+
es256: false,
|
|
131
|
+
signature: ed25519[0].signature,
|
|
132
|
+
message: ed25519[0].message,
|
|
133
|
+
publicKey: ed25519[0].publicKey
|
|
134
|
+
})
|
|
135
|
+
]) : binding.verifyBatch(ed25519.map((item)=>({
|
|
136
|
+
es256: false,
|
|
137
|
+
signature: item.signature,
|
|
138
|
+
message: item.message,
|
|
139
|
+
publicKey: item.publicKey
|
|
140
|
+
}))).then((results)=>assertResultCount(results, ed25519.length));
|
|
141
|
+
const [edResults, esResults] = await Promise.all([
|
|
142
|
+
native,
|
|
143
|
+
Promise.all(es256)
|
|
144
|
+
]);
|
|
145
|
+
let ed = 0;
|
|
146
|
+
let es = 0;
|
|
147
|
+
return algs.map((alg)=>{
|
|
148
|
+
return alg === 'EdDSA' ? edResults[ed++] : esResults[es++];
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
async blake3 ({ input, dkLen }) {
|
|
152
|
+
assertBytes(input);
|
|
153
|
+
assertOutputLength(dkLen);
|
|
154
|
+
return binding.blake3({
|
|
155
|
+
input,
|
|
156
|
+
length: dkLen
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
async blake3Batch ({ inputs, dkLen }) {
|
|
160
|
+
assertOutputLength(dkLen);
|
|
161
|
+
for (const input of inputs)assertBytes(input);
|
|
162
|
+
if (inputs.length === 0) return [];
|
|
163
|
+
const total = inputs.reduce((sum, input)=>sum + input.length, 0);
|
|
164
|
+
if (total <= BLAKE3_SYNC_BATCH_BYTES) return inputs.map((input)=>binding.blake3({
|
|
165
|
+
input,
|
|
166
|
+
length: dkLen
|
|
167
|
+
}));
|
|
168
|
+
const count = inputs.length;
|
|
169
|
+
return assertResultCount(await binding.blake3Batch({
|
|
170
|
+
inputs,
|
|
171
|
+
length: dkLen
|
|
172
|
+
}), count);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The Rust core through Node-API when this platform's binary loads, otherwise `nodeCrypto`.
|
|
178
|
+
* The choice is made once, here; a native failure after construction rejects instead of
|
|
179
|
+
* falling back.
|
|
180
|
+
*/ export function createNapiCrypto(options = {}) {
|
|
181
|
+
const loaded = options.binding !== undefined ? {
|
|
182
|
+
binding: options.binding
|
|
183
|
+
} : loadNapiBinding();
|
|
184
|
+
if (loaded.binding == null) {
|
|
185
|
+
const failure = 'failure' in loaded ? loaded.failure : undefined;
|
|
186
|
+
return {
|
|
187
|
+
...nodeCrypto,
|
|
188
|
+
backend: 'node',
|
|
189
|
+
...failure ? {
|
|
190
|
+
failure
|
|
191
|
+
} : {}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
...napiProvider(loaded.binding),
|
|
196
|
+
backend: 'napi'
|
|
197
|
+
};
|
|
198
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kokuin/crypto-napi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Strict asynchronous crypto provider backed by the Rust core through Node-API, falling back to @kokuin/crypto-node",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/TairuFramework/kokuin",
|
|
8
|
+
"directory": "packages/crypto-napi"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"main": "lib/index.js",
|
|
17
|
+
"types": "lib/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"lib/*",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@kokuin/crypto": "^0.7.0",
|
|
24
|
+
"@kokuin/crypto-node": "^0.7.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@kokuin/crypto-conformance": "^0.0.0",
|
|
28
|
+
"@noble/curves": "^2.4.0",
|
|
29
|
+
"@noble/hashes": "^2.4.0",
|
|
30
|
+
"@types/node": "^26.6.6"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
40
|
+
"build:clean": "del-cli lib",
|
|
41
|
+
"build:js": "del-cli 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
42
|
+
"build:native": "node ../../scripts/build-crypto-napi.mjs",
|
|
43
|
+
"build:types": "del-cli 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
|
|
44
|
+
"test": "pnpm run test:types && pnpm run test:unit",
|
|
45
|
+
"test:native": "vitest run test/native.test.ts",
|
|
46
|
+
"test:types": "tsc --noEmit --skipLibCheck -p tsconfig.test.json",
|
|
47
|
+
"test:unit": "vitest run --exclude test/native.test.ts"
|
|
48
|
+
}
|
|
6
49
|
}
|