Skip to content

Commit 8efb065

Browse files
authored
fix: abort async operations (#3152)
Some async operations do not accept an abort signal. This means calling code can fail to abort a long-running operation if the abort signal fires it's "abort" event while execution is suspended. For example in the following code the signal can fire while the directory is being created and this code would be none the wiser: ```js import fs from 'node:fs/promises' async function doSomthing (signal) { await fs.mkdir() } ``` Instead we need to check if the signal is aborted before starting the async operation (to prevent doing needless work), and after, in case it aborted while the onward call was happening. ```js import fs from 'node:fs/promises' async function doSomthing (signal) { signal.throwIfAborted() await fs.mkdir() signal.throwIfAborted() } ``` This still waits for the operation to complete before checking the signal to see if it has been aborted. Instead we can use `raceSignal` to throw immediately, though the operation will continue in the background. ```js import fs from 'node:fs/promises' import { raceSignal } from 'race-signal' async function doSomthing (signal) { signal.throwIfAborted() await raceSignal(fs.mkdir(), signal) } ``` Synchronous operations have a similar problem when they are able to return optional promises, since the result may be awaited on. In this case we still need to check if the signal was aborted, though we only need to do it once. ```ts interface MyOp<T> { (signal: AbortSignal): Promise<T> | T } const fn: MyOp = (signal) => { signal.throwIfAborted() return 'hello' } // awaiting this shifts the continuation into the microtask queue await fn(AbortSignal.timeout(1_000)) // could use optional await but `fn` cannot tell if it's being awaited on or not // so it always has to check the signal const p = fn(AbortSignal.timeout(1_000)) if (p.then) { await p } ``` This PR applies the above patterns to `@libp2p/crypto` keys, `@libp2p/peer-store` operations and `@libp2p/kad-dht` to ensure the `.provide` and `.findProviders` operations can be successfully aborted.
1 parent 8618d09 commit 8efb065

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+543
-312
lines changed

.github/dependabot.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
version: 2
22
updates:
33
- package-ecosystem: npm
4-
directories:
5-
- "/"
4+
directory: "/"
65
schedule:
76
interval: daily
87
time: "10:00"
@@ -11,7 +10,7 @@ updates:
1110
prefix: "deps"
1211
prefix-development: "chore"
1312
groups:
14-
libp2p-deps: # group all deps that should be updated when Helia deps need updated
13+
libp2p-deps:
1514
patterns:
1615
- "*libp2p*"
1716
- "*multiformats*"

packages/crypto/src/keys/ecdsa/ecdsa.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
55
import { publicKeyToProtobuf } from '../index.js'
66
import { privateKeyToPKIMessage, publicKeyToPKIMessage } from './utils.js'
77
import { hashAndVerify, hashAndSign } from './index.js'
8-
import type { ECDSAPublicKey as ECDSAPublicKeyInterface, ECDSAPrivateKey as ECDSAPrivateKeyInterface } from '@libp2p/interface'
8+
import type { ECDSAPublicKey as ECDSAPublicKeyInterface, ECDSAPrivateKey as ECDSAPrivateKeyInterface, AbortOptions } from '@libp2p/interface'
99
import type { Digest } from 'multiformats/hashes/digest'
1010
import type { Uint8ArrayList } from 'uint8arraylist'
1111

@@ -46,8 +46,8 @@ export class ECDSAPublicKey implements ECDSAPublicKeyInterface {
4646
return uint8ArrayEquals(this.raw, key.raw)
4747
}
4848

49-
async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
50-
return hashAndVerify(this.jwk, sig, data)
49+
async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array, options?: AbortOptions): Promise<boolean> {
50+
return hashAndVerify(this.jwk, sig, data, options)
5151
}
5252
}
5353

@@ -85,7 +85,7 @@ export class ECDSAPrivateKey implements ECDSAPrivateKeyInterface {
8585
return uint8ArrayEquals(this.raw, key.raw)
8686
}
8787

88-
async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
89-
return hashAndSign(this.jwk, message)
88+
async sign (message: Uint8Array | Uint8ArrayList, options?: AbortOptions): Promise<Uint8Array> {
89+
return hashAndSign(this.jwk, message, options)
9090
}
9191
}

packages/crypto/src/keys/ecdsa/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { JWKKeyPair } from '../interface.js'
2+
import type { AbortOptions } from '@libp2p/interface'
23
import type { Uint8ArrayList } from 'uint8arraylist'
34

45
export type Curve = 'P-256' | 'P-384' | 'P-521'
@@ -19,32 +20,38 @@ export async function generateECDSAKey (curve: Curve = 'P-256'): Promise<JWKKeyP
1920
}
2021
}
2122

22-
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
23+
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): Promise<Uint8Array> {
2324
const privateKey = await crypto.subtle.importKey('jwk', key, {
2425
name: 'ECDSA',
2526
namedCurve: key.crv ?? 'P-256'
2627
}, false, ['sign'])
28+
options?.signal?.throwIfAborted()
2729

2830
const signature = await crypto.subtle.sign({
2931
name: 'ECDSA',
3032
hash: {
3133
name: 'SHA-256'
3234
}
3335
}, privateKey, msg.subarray())
36+
options?.signal?.throwIfAborted()
3437

3538
return new Uint8Array(signature, 0, signature.byteLength)
3639
}
3740

38-
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
41+
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): Promise<boolean> {
3942
const publicKey = await crypto.subtle.importKey('jwk', key, {
4043
name: 'ECDSA',
4144
namedCurve: key.crv ?? 'P-256'
4245
}, false, ['verify'])
46+
options?.signal?.throwIfAborted()
4347

44-
return crypto.subtle.verify({
48+
const result = await crypto.subtle.verify({
4549
name: 'ECDSA',
4650
hash: {
4751
name: 'SHA-256'
4852
}
4953
}, publicKey, sig, msg.subarray())
54+
options?.signal?.throwIfAborted()
55+
56+
return result
5057
}

packages/crypto/src/keys/ed25519/ed25519.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
55
import { publicKeyToProtobuf } from '../index.js'
66
import { ensureEd25519Key } from './utils.js'
77
import * as crypto from './index.js'
8-
import type { Ed25519PublicKey as Ed25519PublicKeyInterface, Ed25519PrivateKey as Ed25519PrivateKeyInterface } from '@libp2p/interface'
8+
import type { Ed25519PublicKey as Ed25519PublicKeyInterface, Ed25519PrivateKey as Ed25519PrivateKeyInterface, AbortOptions } from '@libp2p/interface'
99
import type { Digest } from 'multiformats/hashes/digest'
1010
import type { Uint8ArrayList } from 'uint8arraylist'
1111

@@ -37,7 +37,8 @@ export class Ed25519PublicKey implements Ed25519PublicKeyInterface {
3737
return uint8ArrayEquals(this.raw, key.raw)
3838
}
3939

40-
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean {
40+
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array, options?: AbortOptions): boolean {
41+
options?.signal?.throwIfAborted()
4142
return crypto.hashAndVerify(this.raw, sig, data)
4243
}
4344
}
@@ -62,7 +63,8 @@ export class Ed25519PrivateKey implements Ed25519PrivateKeyInterface {
6263
return uint8ArrayEquals(this.raw, key.raw)
6364
}
6465

65-
sign (message: Uint8Array | Uint8ArrayList): Uint8Array {
66+
sign (message: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array {
67+
options?.signal?.throwIfAborted()
6668
return crypto.hashAndSign(this.raw, message)
6769
}
6870
}

packages/crypto/src/keys/ed25519/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function generateKeyFromSeed (seed: Uint8Array): Uint8ArrayKeyPair {
7272
}
7373
}
7474

75-
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Buffer {
75+
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array {
7676
if (!(key instanceof Uint8Array)) {
7777
throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.')
7878
}

packages/crypto/src/keys/rsa/index.browser.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import randomBytes from '../../random-bytes.js'
44
import webcrypto from '../../webcrypto/index.js'
55
import * as utils from './utils.js'
66
import type { JWKKeyPair } from '../interface.js'
7+
import type { AbortOptions } from '@libp2p/interface'
78
import type { Uint8ArrayList } from 'uint8arraylist'
89

910
export const RSAES_PKCS1_V1_5_OID = '1.2.840.113549.1.1.1'
1011
export { utils }
1112

12-
export async function generateRSAKey (bits: number): Promise<JWKKeyPair> {
13+
export async function generateRSAKey (bits: number, options?: AbortOptions): Promise<JWKKeyPair> {
1314
const pair = await webcrypto.get().subtle.generateKey(
1415
{
1516
name: 'RSASSA-PKCS1-v1_5',
@@ -20,8 +21,9 @@ export async function generateRSAKey (bits: number): Promise<JWKKeyPair> {
2021
true,
2122
['sign', 'verify']
2223
)
24+
options?.signal?.throwIfAborted()
2325

24-
const keys = await exportKey(pair)
26+
const keys = await exportKey(pair, options)
2527

2628
return {
2729
privateKey: keys[0],
@@ -31,7 +33,7 @@ export async function generateRSAKey (bits: number): Promise<JWKKeyPair> {
3133

3234
export { randomBytes as getRandomValues }
3335

34-
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
36+
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): Promise<Uint8Array> {
3537
const privateKey = await webcrypto.get().subtle.importKey(
3638
'jwk',
3739
key,
@@ -42,17 +44,19 @@ export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8Array
4244
false,
4345
['sign']
4446
)
47+
options?.signal?.throwIfAborted()
4548

4649
const sig = await webcrypto.get().subtle.sign(
4750
{ name: 'RSASSA-PKCS1-v1_5' },
4851
privateKey,
4952
msg instanceof Uint8Array ? msg : msg.subarray()
5053
)
54+
options?.signal?.throwIfAborted()
5155

5256
return new Uint8Array(sig, 0, sig.byteLength)
5357
}
5458

55-
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
59+
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): Promise<boolean> {
5660
const publicKey = await webcrypto.get().subtle.importKey(
5761
'jwk',
5862
key,
@@ -63,24 +67,31 @@ export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint
6367
false,
6468
['verify']
6569
)
70+
options?.signal?.throwIfAborted()
6671

67-
return webcrypto.get().subtle.verify(
72+
const result = await webcrypto.get().subtle.verify(
6873
{ name: 'RSASSA-PKCS1-v1_5' },
6974
publicKey,
7075
sig,
7176
msg instanceof Uint8Array ? msg : msg.subarray()
7277
)
78+
options?.signal?.throwIfAborted()
79+
80+
return result
7381
}
7482

75-
async function exportKey (pair: CryptoKeyPair): Promise<[JsonWebKey, JsonWebKey]> {
83+
async function exportKey (pair: CryptoKeyPair, options?: AbortOptions): Promise<[JsonWebKey, JsonWebKey]> {
7684
if (pair.privateKey == null || pair.publicKey == null) {
7785
throw new InvalidParametersError('Private and public key are required')
7886
}
7987

80-
return Promise.all([
88+
const result = await Promise.all([
8189
webcrypto.get().subtle.exportKey('jwk', pair.privateKey),
8290
webcrypto.get().subtle.exportKey('jwk', pair.publicKey)
8391
])
92+
options?.signal?.throwIfAborted()
93+
94+
return result
8495
}
8596

8697
export function rsaKeySize (jwk: JsonWebKey): number {

packages/crypto/src/keys/rsa/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import crypto from 'crypto'
2-
import { promisify } from 'util'
1+
import crypto from 'node:crypto'
2+
import { promisify } from 'node:util'
33
import { InvalidParametersError } from '@libp2p/interface'
44
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
55
import randomBytes from '../../random-bytes.js'
66
import * as utils from './utils.js'
77
import type { JWKKeyPair } from '../interface.js'
8+
import type { AbortOptions } from '@libp2p/interface'
89
import type { Uint8ArrayList } from 'uint8arraylist'
910

1011
const keypair = promisify(crypto.generateKeyPair)
@@ -13,13 +14,14 @@ export const RSAES_PKCS1_V1_5_OID = '1.2.840.113549.1.1.1'
1314

1415
export { utils }
1516

16-
export async function generateRSAKey (bits: number): Promise<JWKKeyPair> {
17+
export async function generateRSAKey (bits: number, options?: AbortOptions): Promise<JWKKeyPair> {
1718
// @ts-expect-error node types are missing jwk as a format
1819
const key = await keypair('rsa', {
1920
modulusLength: bits,
2021
publicKeyEncoding: { type: 'pkcs1', format: 'jwk' },
2122
privateKeyEncoding: { type: 'pkcs1', format: 'jwk' }
2223
})
24+
options?.signal?.throwIfAborted()
2325

2426
return {
2527
// @ts-expect-error node types are missing jwk as a format
@@ -31,7 +33,9 @@ export async function generateRSAKey (bits: number): Promise<JWKKeyPair> {
3133

3234
export { randomBytes as getRandomValues }
3335

34-
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
36+
export function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array {
37+
options?.signal?.throwIfAborted()
38+
3539
const hash = crypto.createSign('RSA-SHA256')
3640

3741
if (msg instanceof Uint8Array) {
@@ -46,7 +50,9 @@ export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8Array
4650
return hash.sign({ format: 'jwk', key })
4751
}
4852

49-
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
53+
export function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): boolean {
54+
options?.signal?.throwIfAborted()
55+
5056
const hash = crypto.createVerify('RSA-SHA256')
5157

5258
if (msg instanceof Uint8Array) {

packages/crypto/src/keys/rsa/rsa.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { base58btc } from 'multiformats/bases/base58'
22
import { CID } from 'multiformats/cid'
33
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
44
import { hashAndSign, utils, hashAndVerify } from './index.js'
5-
import type { RSAPublicKey as RSAPublicKeyInterface, RSAPrivateKey as RSAPrivateKeyInterface } from '@libp2p/interface'
5+
import type { RSAPublicKey as RSAPublicKeyInterface, RSAPrivateKey as RSAPrivateKeyInterface, AbortOptions } from '@libp2p/interface'
66
import type { Digest } from 'multiformats/hashes/digest'
77
import type { Uint8ArrayList } from 'uint8arraylist'
88

@@ -45,8 +45,8 @@ export class RSAPublicKey implements RSAPublicKeyInterface {
4545
return uint8ArrayEquals(this.raw, key.raw)
4646
}
4747

48-
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): boolean | Promise<boolean> {
49-
return hashAndVerify(this.jwk, sig, data)
48+
verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array, options?: AbortOptions): boolean | Promise<boolean> {
49+
return hashAndVerify(this.jwk, sig, data, options)
5050
}
5151
}
5252

@@ -77,7 +77,7 @@ export class RSAPrivateKey implements RSAPrivateKeyInterface {
7777
return uint8ArrayEquals(this.raw, key.raw)
7878
}
7979

80-
sign (message: Uint8Array | Uint8ArrayList): Uint8Array | Promise<Uint8Array> {
81-
return hashAndSign(this.jwk, message)
80+
sign (message: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array | Promise<Uint8Array> {
81+
return hashAndSign(this.jwk, message, options)
8282
}
8383
}

packages/crypto/src/keys/secp256k1/index.browser.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { secp256k1 as secp } from '@noble/curves/secp256k1'
22
import { sha256 } from 'multiformats/hashes/sha2'
33
import { SigningError, VerificationError } from '../../errors.js'
44
import { isPromise } from '../../util.js'
5+
import type { AbortOptions } from '@libp2p/interface'
56
import type { Uint8ArrayList } from 'uint8arraylist'
67

78
const PUBLIC_KEY_BYTE_LENGTH = 33
@@ -13,12 +14,20 @@ export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }
1314
/**
1415
* Hash and sign message with private key
1516
*/
16-
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array | Promise<Uint8Array> {
17+
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array | Promise<Uint8Array> {
1718
const p = sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray())
1819

1920
if (isPromise(p)) {
20-
return p.then(({ digest }) => secp.sign(digest, key).toDERRawBytes())
21+
return p
22+
.then(({ digest }) => {
23+
options?.signal?.throwIfAborted()
24+
return secp.sign(digest, key).toDERRawBytes()
25+
})
2126
.catch(err => {
27+
if (err.name === 'AbortError') {
28+
throw err
29+
}
30+
2231
throw new SigningError(String(err))
2332
})
2433
}
@@ -33,17 +42,26 @@ export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList):
3342
/**
3443
* Hash message and verify signature with public key
3544
*/
36-
export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean | Promise<boolean> {
45+
export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): boolean | Promise<boolean> {
3746
const p = sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray())
3847

3948
if (isPromise(p)) {
40-
return p.then(({ digest }) => secp.verify(sig, digest, key))
49+
return p
50+
.then(({ digest }) => {
51+
options?.signal?.throwIfAborted()
52+
return secp.verify(sig, digest, key)
53+
})
4154
.catch(err => {
55+
if (err.name === 'AbortError') {
56+
throw err
57+
}
58+
4259
throw new VerificationError(String(err))
4360
})
4461
}
4562

4663
try {
64+
options?.signal?.throwIfAborted()
4765
return secp.verify(sig, p.digest, key)
4866
} catch (err) {
4967
throw new VerificationError(String(err))

packages/crypto/src/keys/secp256k1/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import crypto from 'node:crypto'
22
import { secp256k1 as secp } from '@noble/curves/secp256k1'
33
import { SigningError, VerificationError } from '../../errors.js'
4+
import type { AbortOptions } from '@libp2p/interface'
45
import type { Uint8ArrayList } from 'uint8arraylist'
56

67
const PUBLIC_KEY_BYTE_LENGTH = 33
@@ -12,7 +13,9 @@ export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }
1213
/**
1314
* Hash and sign message with private key
1415
*/
15-
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Uint8Array {
16+
export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): Uint8Array {
17+
options?.signal?.throwIfAborted()
18+
1619
const hash = crypto.createHash('sha256')
1720

1821
if (msg instanceof Uint8Array) {
@@ -36,7 +39,8 @@ export function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList):
3639
/**
3740
* Hash message and verify signature with public key
3841
*/
39-
export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): boolean {
42+
export function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList, options?: AbortOptions): boolean {
43+
options?.signal?.throwIfAborted()
4044
const hash = crypto.createHash('sha256')
4145

4246
if (msg instanceof Uint8Array) {

0 commit comments

Comments
 (0)