Skip to content

Commit

Permalink
[dynamicIO] Instrument crypto ranadom APIs to be dynamic (#70939)
Browse files Browse the repository at this point in the history
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
gnoff authored and kdy1 committed Oct 10, 2024
1 parent cc95771 commit f3a830d
Show file tree
Hide file tree
Showing 32 changed files with 1,111 additions and 44 deletions.
24 changes: 2 additions & 22 deletions packages/next/src/server/node-environment-extensions/date.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
/**
* We extend `Date` during builds and revalidates to ensure that prerenders don't observe the clock as a source of IO
* When dynamicIO is enabled. The current time is a form of IO even though it resolves synchronously. When dyanmicIO is
* enabled we need to ensure that clock time is excluded from prerenders.
* enabled we need to ensure that clock time is excluded from prerenders unless it is cached.
*
* There is tension here because time is used for both output and introspection. While arbitrary we intend to reserve
* `Date` for output use cases and `performance` for introspection use cases. If you want to measure
* how long something takes use `performance.timeOrigin` and `performance.now()` rather than `Date.now()` for instance.
*
* The extensions here never error nor alter the underlying Date objects, strings, and numbers created and thus should be transparent to callers.
*/
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'

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)
}
}
}
import { io } from './utils'

function createNow(originalNow: typeof Date.now) {
return {
Expand Down
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)
}
}
26 changes: 4 additions & 22 deletions packages/next/src/server/node-environment-extensions/random.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,13 @@
* The extensions here never error nor alter the random generation itself and thus should be transparent to callers.
*/

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'
import { io } from './utils'

const originalRandom = Math.random
const _random = Math.random

Math.random = function random() {
const workUnitStore = workUnitAsyncStorage.getStore()
if (
workUnitStore &&
workUnitStore.type === 'prerender' &&
isDynamicIOPrerender(workUnitStore)
) {
const workStore = workAsyncStorage.getStore()
const route = workStore ? workStore.route : ''
const expression = '`Math.random()`'
if (workStore) {
abortOnSynchronousDynamicDataAccess(route, expression, workUnitStore)
}
}

return originalRandom.apply(null, arguments as any)
io('`Math.random()`')
return _random.apply(null, arguments as any)

// We bind here to alter the `toString` printing to match `Math.random`'s native `toString`.
// eslint-disable-next-line no-extra-bind
Expand Down
21 changes: 21 additions & 0 deletions packages/next/src/server/node-environment-extensions/utils.tsx
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)
}
}
}
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
2 changes: 2 additions & 0 deletions packages/next/src/server/node-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
import './node-environment-baseline'
import './node-environment-extensions/random'
import './node-environment-extensions/date'
import './node-environment-extensions/web-crypto'
import './node-environment-extensions/node-crypto'
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',
},
})
}
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
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()
}
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>
)
}
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))
}
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>
)
}
Loading

0 comments on commit f3a830d

Please sign in to comment.