-
Notifications
You must be signed in to change notification settings - Fork 26.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[dynamicIO] Instrument crypto ranadom APIs to be dynamic (#70939)
crypto random bytes are IO semantically even if they are synchronous. We instrument the sync crypto APIs that allow access to randomness to be be dynamic while prerendering.
- Loading branch information
Showing
32 changed files
with
1,111 additions
and
44 deletions.
There are no files selected for viewing
24 changes: 2 additions & 22 deletions
24
packages/next/src/server/node-environment-extensions/date.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
packages/next/src/server/node-environment-extensions/node-crypto.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/** | ||
* We extend node:crypto APIs during builds and revalidates to ensure that prerenders don't observe random bytes | ||
* When dynamicIO is enabled. Random bytes are a form of IO even if they resolve synchronously. When dyanmicIO is | ||
* enabled we need to ensure that random bytes are excluded from prerenders unless they are cached. | ||
* | ||
* | ||
* The extensions here never error nor alter the underlying return values and thus should be transparent to callers. | ||
*/ | ||
|
||
import { io } from './utils' | ||
|
||
if (process.env.NEXT_RUNTIME === 'edge') { | ||
// nothing to patch | ||
} else { | ||
const nodeCrypto = require('node:crypto') | ||
|
||
// require('node:crypto').getRandomValues is an alias for | ||
// crypto.getRandomValues which is extended in web-crypto.tsx | ||
|
||
// require('node:crypto').randomUUID is not an alias for crypto.randomUUID | ||
const _randomUUID = nodeCrypto.randomUUID | ||
nodeCrypto.randomUUID = function randomUUID() { | ||
io("`require('node:crypto').randomUUID()`") | ||
return _randomUUID.apply(this, arguments as any) | ||
} | ||
|
||
const _randomBytes = nodeCrypto.randomBytes | ||
nodeCrypto.randomBytes = function randomBytes() { | ||
if (typeof arguments[1] !== 'function') { | ||
// randomBytes is sync if the second arg is undefined | ||
io("`require('node:crypto').randomBytes(size)`") | ||
} | ||
return _randomBytes.apply(this, arguments as any) | ||
} | ||
|
||
const _randomFillSync = nodeCrypto.randomFillSync | ||
nodeCrypto.randomFillSync = function randomFillSync() { | ||
io("`require('node:crypto').randomFillSync(...)`") | ||
return _randomFillSync.apply(this, arguments as any) | ||
} | ||
|
||
const _randomInt = nodeCrypto.randomInt | ||
nodeCrypto.randomInt = function randomInt() { | ||
if (typeof arguments[2] !== 'function') { | ||
// randomInt is sync if the third arg is undefined | ||
io("`require('node:crypto').randomInt(min, max)`") | ||
} | ||
return _randomInt.apply(this, arguments as any) | ||
} | ||
|
||
const _generatePrimeSync = nodeCrypto.generatePrimeSync | ||
nodeCrypto.generatePrimeSync = function generatePrimeSync() { | ||
io("`require('node:crypto').generatePrimeSync(...)`") | ||
return _generatePrimeSync.apply(this, arguments as any) | ||
} | ||
|
||
const _generateKeyPairSync = nodeCrypto.generateKeyPairSync | ||
nodeCrypto.generateKeyPairSync = function generateKeyPairSync() { | ||
io("`require('node:crypto').generateKeyPairSync(...)`") | ||
return _generateKeyPairSync.apply(this, arguments as any) | ||
} | ||
|
||
const _generateKeySync = nodeCrypto.generateKeySync | ||
nodeCrypto.generateKeySync = function generateKeySync() { | ||
io("`require('node:crypto').generateKeySync(...)`") | ||
return _generateKeySync.apply(this, arguments as any) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
packages/next/src/server/node-environment-extensions/utils.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { workAsyncStorage } from '../../client/components/work-async-storage.external' | ||
import { | ||
isDynamicIOPrerender, | ||
workUnitAsyncStorage, | ||
} from '../app-render/work-unit-async-storage.external' | ||
import { abortOnSynchronousDynamicDataAccess } from '../app-render/dynamic-rendering' | ||
|
||
export function io(expression: string) { | ||
const workUnitStore = workUnitAsyncStorage.getStore() | ||
if ( | ||
workUnitStore && | ||
workUnitStore.type === 'prerender' && | ||
isDynamicIOPrerender(workUnitStore) | ||
) { | ||
const workStore = workAsyncStorage.getStore() | ||
const route = workStore ? workStore.route : '' | ||
if (workStore) { | ||
abortOnSynchronousDynamicDataAccess(route, expression, workUnitStore) | ||
} | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
packages/next/src/server/node-environment-extensions/web-crypto.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* We extend Web Crypto APIs during builds and revalidates to ensure that prerenders don't observe random bytes | ||
* When dynamicIO is enabled. Random bytes are a form of IO even if they resolve synchronously. When dyanmicIO is | ||
* enabled we need to ensure that random bytes are excluded from prerenders unless they are cached. | ||
* | ||
* | ||
* The extensions here never error nor alter the underlying return values and thus should be transparent to callers. | ||
*/ | ||
|
||
import { io } from './utils' | ||
|
||
let webCrypto: typeof crypto | ||
if (process.env.NEXT_RUNTIME === 'edge') { | ||
webCrypto = crypto | ||
} else { | ||
if (typeof crypto === 'undefined') { | ||
webCrypto = require('node:crypto').webcrypto | ||
} else { | ||
webCrypto = crypto | ||
} | ||
} | ||
|
||
const originalGetRandomValues = webCrypto.getRandomValues | ||
webCrypto.getRandomValues = function getRandomValues() { | ||
io('`crypto.getRandomValues()`') | ||
return originalGetRandomValues.apply(webCrypto, arguments as any) | ||
} | ||
|
||
const _randomUUID = webCrypto.randomUUID | ||
webCrypto.randomUUID = function randomUUID() { | ||
io('`crypto.randomUUID()`') | ||
return _randomUUID.apply(webCrypto, arguments as any) | ||
} as typeof _randomUUID |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
test/e2e/app-dir/dynamic-io/app/node-crypto/generate-key-pair-sync/cached/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import crypto from 'node:crypto' | ||
|
||
import { SentinelValue } from '../../../getSentinelValue' | ||
|
||
export default async function Page() { | ||
const first = await getGeneratedKeyPair(1) | ||
const second = await getGeneratedKeyPair(2) | ||
return ( | ||
<div> | ||
<dl> | ||
<dt> | ||
[first] require('node:crypto').generateKeyPairSync(type, options) | ||
</dt> | ||
<dd id="first">{first.publicKey}</dd> | ||
<dt> | ||
[second] require('node:crypto').generateKeyPairSync(type, options) | ||
</dt> | ||
<dd id="second">{second.publicKey}</dd> | ||
</dl> | ||
<span id="page"> | ||
<SentinelValue /> | ||
</span> | ||
</div> | ||
) | ||
} | ||
|
||
async function getGeneratedKeyPair(_nonce: number) { | ||
'use cache' | ||
return crypto.generateKeyPairSync('rsa', { | ||
modulusLength: 512, | ||
publicKeyEncoding: { | ||
type: 'spki', | ||
format: 'pem', | ||
}, | ||
privateKeyEncoding: { | ||
type: 'pkcs8', | ||
format: 'pem', | ||
}, | ||
}) | ||
} |
38 changes: 38 additions & 0 deletions
38
test/e2e/app-dir/dynamic-io/app/node-crypto/generate-key-pair-sync/uncached/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import crypto from 'node:crypto' | ||
|
||
import { SentinelValue } from '../../../getSentinelValue' | ||
|
||
export default async function Page() { | ||
await new Promise((r) => process.nextTick(r)) | ||
const first = crypto.generateKeyPairSync('rsa', keyGenOptions) | ||
const second = crypto.generateKeyPairSync('rsa', keyGenOptions) | ||
return ( | ||
<div> | ||
<dl> | ||
<dt> | ||
[first] require('node:crypto').generateKeyPairSync(type, options) | ||
</dt> | ||
<dd id="first">{first.publicKey}</dd> | ||
<dt> | ||
[second] require('node:crypto').generateKeyPairSync(type, options) | ||
</dt> | ||
<dd id="second">{second.publicKey}</dd> | ||
</dl> | ||
<span id="page"> | ||
<SentinelValue /> | ||
</span> | ||
</div> | ||
) | ||
} | ||
|
||
const keyGenOptions = { | ||
modulusLength: 512, | ||
publicKeyEncoding: { | ||
type: 'spki', | ||
format: 'pem', | ||
}, | ||
privateKeyEncoding: { | ||
type: 'pkcs8', | ||
format: 'pem', | ||
}, | ||
} as const |
30 changes: 30 additions & 0 deletions
30
test/e2e/app-dir/dynamic-io/app/node-crypto/generate-key-sync/cached/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import crypto from 'node:crypto' | ||
|
||
import { SentinelValue } from '../../../getSentinelValue' | ||
|
||
export default async function Page() { | ||
const first = await getGeneratedKey(1) | ||
const second = await getGeneratedKey(2) | ||
return ( | ||
<div> | ||
<dl> | ||
<dt>[first] require('node:crypto').generateKeySync(type, options)</dt> | ||
<dd id="first">{first.toString()}</dd> | ||
<dt>[second] require('node:crypto').generateKeySync(type, options)</dt> | ||
<dd id="second">{second.toString()}</dd> | ||
</dl> | ||
<span id="page"> | ||
<SentinelValue /> | ||
</span> | ||
</div> | ||
) | ||
} | ||
|
||
async function getGeneratedKey(_nonce: number) { | ||
'use cache' | ||
return crypto | ||
.generateKeySync('hmac', { | ||
length: 512, | ||
}) | ||
.export() | ||
} |
30 changes: 30 additions & 0 deletions
30
test/e2e/app-dir/dynamic-io/app/node-crypto/generate-key-sync/uncached/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import crypto from 'node:crypto' | ||
|
||
import { SentinelValue } from '../../../getSentinelValue' | ||
|
||
export default async function Page() { | ||
await new Promise((r) => process.nextTick(r)) | ||
const first = crypto | ||
.generateKeySync('hmac', { | ||
length: 512, | ||
}) | ||
.export() | ||
const second = crypto | ||
.generateKeySync('hmac', { | ||
length: 512, | ||
}) | ||
.export() | ||
return ( | ||
<div> | ||
<dl> | ||
<dt>[first] require('node:crypto').generateKeySync(type, options)</dt> | ||
<dd id="first">{first.toString('hex')}</dd> | ||
<dt>[second] require('node:crypto').generateKeySync(type, options)</dt> | ||
<dd id="second">{second.toString('hex')}</dd> | ||
</dl> | ||
<span id="page"> | ||
<SentinelValue /> | ||
</span> | ||
</div> | ||
) | ||
} |
26 changes: 26 additions & 0 deletions
26
test/e2e/app-dir/dynamic-io/app/node-crypto/generate-prime-sync/cached/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import crypto from 'node:crypto' | ||
|
||
import { SentinelValue } from '../../../getSentinelValue' | ||
|
||
export default async function Page() { | ||
const first = await getGeneratedPrime(1) | ||
const second = await getGeneratedPrime(2) | ||
return ( | ||
<div> | ||
<dl> | ||
<dt>[first] require('node:crypto').generatePrimeSync(size)</dt> | ||
<dd id="first">{first.toString()}</dd> | ||
<dt>[second] require('node:crypto').generatePrimeSync(size)</dt> | ||
<dd id="second">{second.toString()}</dd> | ||
</dl> | ||
<span id="page"> | ||
<SentinelValue /> | ||
</span> | ||
</div> | ||
) | ||
} | ||
|
||
async function getGeneratedPrime(_nonce: number) { | ||
'use cache' | ||
return new Uint8Array(crypto.generatePrimeSync(128)) | ||
} |
22 changes: 22 additions & 0 deletions
22
test/e2e/app-dir/dynamic-io/app/node-crypto/generate-prime-sync/uncached/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import crypto from 'node:crypto' | ||
|
||
import { SentinelValue } from '../../../getSentinelValue' | ||
|
||
export default async function Page() { | ||
await new Promise((r) => process.nextTick(r)) | ||
const first = new Uint8Array(crypto.generatePrimeSync(128)) | ||
const second = new Uint8Array(crypto.generatePrimeSync(128)) | ||
return ( | ||
<div> | ||
<dl> | ||
<dt>[first] require('node:crypto').generatePrimeSync(size)</dt> | ||
<dd id="first">{first.toString()}</dd> | ||
<dt>[second] require('node:crypto').generatePrimeSync(size)</dt> | ||
<dd id="second">{second.toString()}</dd> | ||
</dl> | ||
<span id="page"> | ||
<SentinelValue /> | ||
</span> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.