From 12f2a09b8528bd524ea1aefffded4a96990af10b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Jan 2022 15:50:01 -0500 Subject: [PATCH 01/60] add CSP types --- packages/kit/types/config.d.ts | 4 ++ packages/kit/types/csp.d.ts | 115 +++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 packages/kit/types/csp.d.ts diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index 788bef4cea3b..8095c492867f 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -1,5 +1,6 @@ import { CompileOptions } from 'svelte/types/compiler/interfaces'; import { UserConfig as ViteConfig } from 'vite'; +import { CspDirectives } from './csp'; import { RecursiveRequired } from './helper'; import { HttpMethod, Logger, RouteSegment, TrailingSlash } from './internal'; @@ -117,6 +118,9 @@ export interface Config { adapter?: Adapter; amp?: boolean; appDir?: string; + csp?: { + directives?: CspDirectives; + }; files?: { assets?: string; hooks?: string; diff --git a/packages/kit/types/csp.d.ts b/packages/kit/types/csp.d.ts new file mode 100644 index 000000000000..5b390dd12a9f --- /dev/null +++ b/packages/kit/types/csp.d.ts @@ -0,0 +1,115 @@ +/** + * Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts + * + * MIT License + * + * Copyright (c) 2021-present, Joshua Hemphill + * Copyright (c) 2021, Tecnico Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +type SchemeSource = 'http:' | 'https:' | 'data:' | 'mediastream:' | 'blob:' | 'filesystem:'; + +type HostProtocolSchemes = `${string}://` | ''; +type PortScheme = `:${number}` | '' | ':*'; +/** Can actually be any string, but typed more explicitly to + * restrict the combined optional types of Source from collapsing to just bing `string` */ +type HostNameScheme = `${string}.${string}` | `localhost`; +type HostSource = `${HostProtocolSchemes}${HostNameScheme}${PortScheme}`; + +type CryptoSource = `${'nonce' | 'sha256' | 'sha384' | 'sha512'}-${string}`; + +type BaseSource = 'self' | 'unsafe-eval' | 'unsafe-hashes' | 'unsafe-inline' | 'none'; + +type Source = HostSource | SchemeSource | CryptoSource | BaseSource; +type Sources = Source[]; + +type ActionSource = 'strict-dynamic' | 'report-sample'; + +type FrameSource = HostSource | SchemeSource | 'self' | 'none'; + +type HttpDelineator = '/' | '?' | '#' | '\\'; +type UriPath = `${HttpDelineator}${string}`; + +export type CspDirectives = { + 'child-src'?: Sources; + 'default-src'?: Sources; + 'frame-src'?: Sources; + 'worker-src'?: Sources; + 'connect-src'?: Sources; + 'font-src'?: Sources; + 'img-src'?: Sources; + 'manifest-src'?: Sources; + 'media-src'?: Sources; + 'object-src'?: Sources; + 'prefetch-src'?: Sources; + 'script-src'?: Sources; + 'script-src-elem'?: Sources; + 'script-src-attr'?: Sources; + 'style-src'?: Sources; + 'style-src-elem'?: Sources; + 'style-src-attr'?: Sources; + 'base-uri'?: Array; + sandbox?: Array< + | 'allow-downloads-without-user-activation' + | 'allow-forms' + | 'allow-modals' + | 'allow-orientation-lock' + | 'allow-pointer-lock' + | 'allow-popups' + | 'allow-popups-to-escape-sandbox' + | 'allow-presentation' + | 'allow-same-origin' + | 'allow-scripts' + | 'allow-storage-access-by-user-activation' + | 'allow-top-navigation' + | 'allow-top-navigation-by-user-activation' + >; + 'form-action'?: Sources; + 'frame-ancestors'?: Array; + 'navigate-to'?: Sources; + 'report-uri'?: UriPath[]; + 'report-to'?: string[]; + + 'require-trusted-types-for'?: Array<'script'>; + 'trusted-types'?: Array<'none' | 'allow-duplicates' | '*' | string>; + 'upgrade-insecure-requests'?: boolean; + + /** @deprecated */ + 'require-sri-for'?: Array<'script' | 'style' | 'script style'>; + + /** @deprecated */ + 'block-all-mixed-content'?: boolean; + + /** @deprecated */ + 'plugin-types'?: Array<`${string}/${string}` | 'none'>; + + /** @deprecated */ + referrer?: Array< + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'origin' + | 'origin-when-cross-origin' + | 'same-origin' + | 'strict-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url' + | 'none' + >; +}; From 74a3432e85be1d2e8a1cc43e163db3faffbabf97 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Jan 2022 16:52:26 -0500 Subject: [PATCH 02/60] add csp stuff to config --- .../fixtures/default/svelte.config.js | 0 .../fixtures/export-string/svelte.config.js | 0 packages/kit/src/core/config/index.spec.js | 225 ++++++++++-------- packages/kit/src/core/config/options.js | 50 ++++ packages/kit/src/core/config/test/index.js | 84 ------- .../kit/test/apps/options/svelte.config.js | 5 + packages/kit/types/config.d.ts | 1 + 7 files changed, 179 insertions(+), 186 deletions(-) rename packages/kit/src/core/config/{test => }/fixtures/default/svelte.config.js (100%) rename packages/kit/src/core/config/{test => }/fixtures/export-string/svelte.config.js (100%) delete mode 100644 packages/kit/src/core/config/test/index.js diff --git a/packages/kit/src/core/config/test/fixtures/default/svelte.config.js b/packages/kit/src/core/config/fixtures/default/svelte.config.js similarity index 100% rename from packages/kit/src/core/config/test/fixtures/default/svelte.config.js rename to packages/kit/src/core/config/fixtures/default/svelte.config.js diff --git a/packages/kit/src/core/config/test/fixtures/export-string/svelte.config.js b/packages/kit/src/core/config/fixtures/export-string/svelte.config.js similarity index 100% rename from packages/kit/src/core/config/test/fixtures/export-string/svelte.config.js rename to packages/kit/src/core/config/fixtures/export-string/svelte.config.js diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index df7f0a0e527a..79168d35c8e9 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -1,8 +1,99 @@ +import { join } from 'path'; +import { fileURLToPath } from 'url'; import { test } from 'uvu'; import * as assert from 'uvu/assert'; - import { remove_keys } from '../../utils/object.js'; -import { validate_config } from './index.js'; +import { validate_config, load_config } from './index.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = join(__filename, '..'); + +const get_defaults = (prefix = '') => ({ + extensions: ['.svelte'], + kit: { + adapter: null, + amp: false, + appDir: '_app', + csp: { + mode: 'auto', + directives: { + 'child-src': [], + 'default-src': [], + 'frame-src': [], + 'worker-src': [], + 'connect-src': [], + 'font-src': [], + 'img-src': [], + 'manifest-src': [], + 'media-src': [], + 'object-src': [], + 'prefetch-src': [], + 'script-src': [], + 'script-src-elem': [], + 'script-src-attr': [], + 'style-src': [], + 'style-src-elem': [], + 'style-src-attr': [], + 'base-uri': [], + sandbox: [], + 'form-action': [], + 'frame-ancestors': [], + 'navigate-to': [], + 'report-uri': [], + 'report-to': [], + 'require-trusted-types-for': [], + 'trusted-types': [], + 'upgrade-insecure-requests': false, + 'require-sri-for': [], + 'block-all-mixed-content': false, + 'plugin-types': [], + referrer: [] + } + }, + files: { + assets: prefix + 'static', + hooks: prefix + 'src/hooks', + lib: prefix + 'src/lib', + routes: prefix + 'src/routes', + serviceWorker: prefix + 'src/service-worker', + template: prefix + 'src/app.html' + }, + floc: false, + headers: undefined, + host: undefined, + hydrate: true, + inlineStyleThreshold: 0, + methodOverride: { + parameter: '_method', + allowed: [] + }, + package: { + dir: 'package', + emitTypes: true + }, + serviceWorker: { + register: true + }, + paths: { + base: '', + assets: '' + }, + prerender: { + concurrency: 1, + crawl: true, + enabled: true, + entries: ['*'], + force: undefined, + onError: 'fail', + pages: undefined + }, + protocol: undefined, + router: true, + ssr: null, + target: null, + trailingSlash: 'never' + } +}); test('fills in defaults', () => { const validated = validate_config({}); @@ -14,56 +105,7 @@ test('fills in defaults', () => { remove_keys(validated, ([, v]) => typeof v === 'function'); - assert.equal(validated, { - extensions: ['.svelte'], - kit: { - adapter: null, - amp: false, - appDir: '_app', - files: { - assets: 'static', - hooks: 'src/hooks', - lib: 'src/lib', - routes: 'src/routes', - serviceWorker: 'src/service-worker', - template: 'src/app.html' - }, - floc: false, - headers: undefined, - host: undefined, - hydrate: true, - inlineStyleThreshold: 0, - methodOverride: { - parameter: '_method', - allowed: [] - }, - package: { - dir: 'package', - emitTypes: true - }, - serviceWorker: { - register: true - }, - paths: { - base: '', - assets: '' - }, - prerender: { - concurrency: 1, - crawl: true, - enabled: true, - entries: ['*'], - force: undefined, - onError: 'fail', - pages: undefined - }, - protocol: undefined, - router: true, - ssr: null, - target: null, - trailingSlash: 'never' - } - }); + assert.equal(validated, get_defaults()); }); test('errors on invalid values', () => { @@ -123,56 +165,10 @@ test('fills in partial blanks', () => { remove_keys(validated, ([, v]) => typeof v === 'function'); - assert.equal(validated, { - extensions: ['.svelte'], - kit: { - adapter: null, - amp: false, - appDir: '_app', - files: { - assets: 'public', - hooks: 'src/hooks', - lib: 'src/lib', - routes: 'src/routes', - serviceWorker: 'src/service-worker', - template: 'src/app.html' - }, - floc: false, - headers: undefined, - host: undefined, - hydrate: true, - inlineStyleThreshold: 0, - methodOverride: { - parameter: '_method', - allowed: [] - }, - package: { - dir: 'package', - emitTypes: true - }, - serviceWorker: { - register: true - }, - paths: { - base: '', - assets: '' - }, - prerender: { - concurrency: 1, - crawl: true, - enabled: true, - entries: ['*'], - force: undefined, - onError: 'fail', - pages: undefined - }, - protocol: undefined, - router: true, - ssr: null, - target: null, - trailingSlash: 'never' - } - }); + const config = get_defaults(); + config.kit.files.assets = 'public'; + + assert.equal(validated, config); }); test('fails if kit.appDir is blank', () => { @@ -327,4 +323,29 @@ validate_paths( } ); +test('load default config (esm)', async () => { + const cwd = join(__dirname, 'fixtures/default'); + + const config = await load_config({ cwd }); + remove_keys(config, ([, v]) => typeof v === 'function'); + + assert.equal(config, get_defaults(cwd + '/')); +}); + +test('errors on loading config with incorrect default export', async () => { + let message = null; + + try { + const cwd = join(__dirname, 'fixtures', 'export-string'); + await load_config({ cwd }); + } catch (/** @type {any} */ e) { + message = e.message; + } + + assert.equal( + message, + 'svelte.config.js must have a configuration object as its default export. See https://kit.svelte.dev/docs#configuration' + ); +}); + test.run(); diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 96d1b8ad9ca1..c34bd4af11f2 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -55,6 +55,43 @@ const options = object( return input; }), + csp: object({ + mode: list(['auto', 'hash', 'nonce']), + directives: object({ + 'child-src': string_array(), + 'default-src': string_array(), + 'frame-src': string_array(), + 'worker-src': string_array(), + 'connect-src': string_array(), + 'font-src': string_array(), + 'img-src': string_array(), + 'manifest-src': string_array(), + 'media-src': string_array(), + 'object-src': string_array(), + 'prefetch-src': string_array(), + 'script-src': string_array(), + 'script-src-elem': string_array(), + 'script-src-attr': string_array(), + 'style-src': string_array(), + 'style-src-elem': string_array(), + 'style-src-attr': string_array(), + 'base-uri': string_array(), + sandbox: string_array(), + 'form-action': string_array(), + 'frame-ancestors': string_array(), + 'navigate-to': string_array(), + 'report-uri': string_array(), + 'report-to': string_array(), + 'require-trusted-types-for': string_array(), + 'trusted-types': string_array(), + 'upgrade-insecure-requests': boolean(false), + 'require-sri-for': string_array(), + 'block-all-mixed-content': boolean(false), + 'plugin-types': string_array(), + referrer: string_array() + }) + }), + files: object({ assets: string('static'), hooks: string('src/hooks'), @@ -312,6 +349,19 @@ function string(fallback, allow_empty = true) { }); } +/** + * @returns {Validator} + */ +function string_array() { + return validate([], (input, keypath) => { + if (!Array.isArray(input) || input.some((value) => typeof value !== 'string')) { + throw new Error(`${keypath} must be an array of strings, if specified`); + } + + return input; + }); +} + /** * @param {number} fallback * @returns {Validator} diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js deleted file mode 100644 index 02cb1fd55703..000000000000 --- a/packages/kit/src/core/config/test/index.js +++ /dev/null @@ -1,84 +0,0 @@ -import { join } from 'path'; -import { fileURLToPath } from 'url'; - -import * as assert from 'uvu/assert'; -import { test } from 'uvu'; - -import { remove_keys } from '../../../utils/object.js'; -import { load_config } from '../index.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = join(__filename, '..'); - -test('load default config (esm)', async () => { - const cwd = join(__dirname, 'fixtures/default'); - - const config = await load_config({ cwd }); - remove_keys(config, ([, v]) => typeof v === 'function'); - - assert.equal(config, { - extensions: ['.svelte'], - kit: { - adapter: null, - amp: false, - appDir: '_app', - files: { - assets: join(cwd, 'static'), - hooks: join(cwd, 'src/hooks'), - lib: join(cwd, 'src/lib'), - routes: join(cwd, 'src/routes'), - serviceWorker: join(cwd, 'src/service-worker'), - template: join(cwd, 'src/app.html') - }, - floc: false, - headers: undefined, - host: undefined, - hydrate: true, - inlineStyleThreshold: 0, - methodOverride: { - parameter: '_method', - allowed: [] - }, - package: { - dir: 'package', - emitTypes: true - }, - serviceWorker: { - register: true - }, - paths: { base: '', assets: '' }, - prerender: { - concurrency: 1, - crawl: true, - enabled: true, - entries: ['*'], - force: undefined, - onError: 'fail', - pages: undefined - }, - protocol: undefined, - router: true, - ssr: null, - target: null, - trailingSlash: 'never' - } - }); -}); - -test('errors on loading config with incorrect default export', async () => { - let message = null; - - try { - const cwd = join(__dirname, 'fixtures', 'export-string'); - await load_config({ cwd }); - } catch (/** @type {any} */ e) { - message = e.message; - } - - assert.equal( - message, - 'svelte.config.js must have a configuration object as its default export. See https://kit.svelte.dev/docs#configuration' - ); -}); - -test.run(); diff --git a/packages/kit/test/apps/options/svelte.config.js b/packages/kit/test/apps/options/svelte.config.js index f916490ddc01..e11dd0b93911 100644 --- a/packages/kit/test/apps/options/svelte.config.js +++ b/packages/kit/test/apps/options/svelte.config.js @@ -4,6 +4,11 @@ import path from 'path'; const config = { extensions: ['.jesuslivesineveryone', '.whokilledthemuffinman', '.svelte.md', '.svelte'], kit: { + csp: { + directives: { + 'script-src': ['self'] + } + }, files: { assets: 'public', lib: 'source/components', diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index 8095c492867f..4fadf2ee41c5 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -119,6 +119,7 @@ export interface Config { amp?: boolean; appDir?: string; csp?: { + mode?: 'hash' | 'nonce' | 'auto'; directives?: CspDirectives; }; files?: { From 39c376d43f8ceb897cfe74897854a573df493ed6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Jan 2022 19:06:05 -0500 Subject: [PATCH 03/60] add csp to SSRRenderOptions --- packages/kit/types/internal.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index f3e6422e5604..7163d61424f0 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -1,4 +1,5 @@ import { OutputAsset, OutputChunk } from 'rollup'; +import { ValidatedConfig } from './config'; import { InternalApp, SSRManifest } from './app'; import { Fallthrough, RequestHandler } from './endpoint'; import { Either } from './helper'; @@ -115,6 +116,7 @@ export interface SSRNode { export interface SSRRenderOptions { amp: boolean; + csp: ValidatedConfig['kit']['csp']; dev: boolean; floc: boolean; get_stack: (error: Error) => string | undefined; From a767f4cc9a5178cddfe2f9f4e4da4d6859a53ab2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Jan 2022 19:18:42 -0500 Subject: [PATCH 04/60] lay some groundwork --- packages/kit/src/core/build/build_server.js | 1 + packages/kit/src/core/config/index.spec.js | 58 +++++++------- packages/kit/src/core/config/options.js | 7 +- packages/kit/src/core/dev/plugin.js | 1 + .../kit/src/runtime/server/page/render.js | 77 +++++++++++-------- 5 files changed, 81 insertions(+), 63 deletions(-) diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index 0e8f9b6cbd1d..de0c337ec0a6 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -60,6 +60,7 @@ export class App { this.options = { amp: ${config.kit.amp}, + csp: ${s(config.kit.csp)}, dev: false, floc: ${config.kit.floc}, get_stack: error => String(error), // for security diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 79168d35c8e9..7f33e3452506 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -17,37 +17,37 @@ const get_defaults = (prefix = '') => ({ csp: { mode: 'auto', directives: { - 'child-src': [], - 'default-src': [], - 'frame-src': [], - 'worker-src': [], - 'connect-src': [], - 'font-src': [], - 'img-src': [], - 'manifest-src': [], - 'media-src': [], - 'object-src': [], - 'prefetch-src': [], - 'script-src': [], - 'script-src-elem': [], - 'script-src-attr': [], - 'style-src': [], - 'style-src-elem': [], - 'style-src-attr': [], - 'base-uri': [], - sandbox: [], - 'form-action': [], - 'frame-ancestors': [], - 'navigate-to': [], - 'report-uri': [], - 'report-to': [], - 'require-trusted-types-for': [], - 'trusted-types': [], + 'child-src': undefined, + 'default-src': undefined, + 'frame-src': undefined, + 'worker-src': undefined, + 'connect-src': undefined, + 'font-src': undefined, + 'img-src': undefined, + 'manifest-src': undefined, + 'media-src': undefined, + 'object-src': undefined, + 'prefetch-src': undefined, + 'script-src': undefined, + 'script-src-elem': undefined, + 'script-src-attr': undefined, + 'style-src': undefined, + 'style-src-elem': undefined, + 'style-src-attr': undefined, + 'base-uri': undefined, + sandbox: undefined, + 'form-action': undefined, + 'frame-ancestors': undefined, + 'navigate-to': undefined, + 'report-uri': undefined, + 'report-to': undefined, + 'require-trusted-types-for': undefined, + 'trusted-types': undefined, 'upgrade-insecure-requests': false, - 'require-sri-for': [], + 'require-sri-for': undefined, 'block-all-mixed-content': false, - 'plugin-types': [], - referrer: [] + 'plugin-types': undefined, + referrer: undefined } }, files: { diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index c34bd4af11f2..d9450b3a3c66 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -350,10 +350,13 @@ function string(fallback, allow_empty = true) { } /** + * @param {string[] | undefined} [fallback] * @returns {Validator} */ -function string_array() { - return validate([], (input, keypath) => { +function string_array(fallback) { + return validate(fallback, (input, keypath) => { + if (input === undefined) return input; + if (!Array.isArray(input) || input.some((value) => typeof value !== 'string')) { throw new Error(`${keypath} must be an array of strings, if specified`); } diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index 7fa63353e2e4..ffa994034776 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -224,6 +224,7 @@ export async function create_plugin(config, cwd) { }), { amp: config.kit.amp, + csp: config.kit.csp, dev: true, floc: config.kit.floc, get_stack: (error) => { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 23b2de5c3758..b4a2f2a10a99 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -36,8 +36,8 @@ export async function render_response({ ssr, stuff }) { - const css = new Set(options.manifest._.entry.css); - const js = new Set(options.manifest._.entry.js); + const stylesheets = new Set(options.manifest._.entry.css); + const modulepreloads = new Set(options.manifest._.entry.js); /** @type {Map} */ const styles = new Map(); @@ -55,8 +55,8 @@ export async function render_response({ if (ssr) { branch.forEach(({ node, loaded, fetched, uses_credentials }) => { - if (node.css) node.css.forEach((url) => css.add(url)); - if (node.js) node.js.forEach((url) => js.add(url)); + if (node.css) node.css.forEach((url) => stylesheets.add(url)); + if (node.js) node.js.forEach((url) => modulepreloads.add(url)); if (node.styles) Object.entries(node.styles).forEach(([k, v]) => styles.set(k, v)); // TODO probably better if `fetched` wasn't populated unless `hydrate` @@ -128,6 +128,39 @@ export async function render_response({ const inlined_style = Array.from(styles.values()).join('\n'); + const needs_scripts_csp = + options.csp.directives['script-src'] && + options.csp.directives['script-src'].filter((value) => value !== 'unsafe-inline').length > 0; + + const needs_styles_csp = + options.csp.directives['style-src'] && + options.csp.directives['style-src'].filter((value) => value !== 'unsafe-inline').length > 0; + + // prettier-ignore + const init = ` + import { start } from ${s(options.prefix + options.manifest._.entry.file)}; + start({ + target: ${options.target ? `document.querySelector(${s(options.target)})` : 'document.body'}, + paths: ${s(options.paths)}, + session: ${try_serialize($session, (error) => { + throw new Error(`Failed to serialize session data: ${error.message}`); + })}, + route: ${!!page_config.router}, + spa: ${!ssr}, + trailing_slash: ${s(options.trailing_slash)}, + hydrate: ${ssr && page_config.hydrate ? `{ + status: ${status}, + error: ${serialize_error(error)}, + nodes: [ + ${(branch || []) + .map(({ node }) => `import(${s(options.prefix + node.entry)})`) + .join(',\n\t\t\t\t\t\t')} + ], + url: new URL(${s(url.href)}), + params: ${devalue(params)} + }` : 'null'} + });`; + if (state.prerender) { if (maxage) { head += ``; @@ -150,43 +183,23 @@ export async function render_response({ } } else { if (inlined_style) { - head += `\n\t${inlined_style}`; + const attributes = []; + if (options.dev) attributes.push(' data-svelte'); + + head += `\n\t${inlined_style}`; } // prettier-ignore - head += Array.from(css) + head += Array.from(stylesheets) .map((dep) => `\n\t`) .join(''); if (page_config.router || page_config.hydrate) { - head += Array.from(js) + head += Array.from(modulepreloads) .map((dep) => `\n\t`) .join(''); - // prettier-ignore + head += ` - `; + `; body += serialized_data .map(({ url, body, json }) => { From 77c5665a48a09199deba20bd05a09db5d2661cc9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Jan 2022 19:44:34 -0500 Subject: [PATCH 05/60] fall back to default-src --- packages/kit/src/runtime/server/page/render.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index b4a2f2a10a99..7896e910180f 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -128,13 +128,14 @@ export async function render_response({ const inlined_style = Array.from(styles.values()).join('\n'); + const script_src = options.csp.directives['script-src'] || options.csp.directives['default-src']; + const style_src = options.csp.directives['style-src'] || options.csp.directives['default-src']; + const needs_scripts_csp = - options.csp.directives['script-src'] && - options.csp.directives['script-src'].filter((value) => value !== 'unsafe-inline').length > 0; + script_src && script_src.filter((value) => value !== 'unsafe-inline').length > 0; const needs_styles_csp = - options.csp.directives['style-src'] && - options.csp.directives['style-src'].filter((value) => value !== 'unsafe-inline').length > 0; + style_src && style_src.filter((value) => value !== 'unsafe-inline').length > 0; // prettier-ignore const init = ` From 043d0642280990dfabb0dc02ee9e54ff66f2b92b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 22 Jan 2022 07:46:07 -0500 Subject: [PATCH 06/60] more stuff --- .../kit/src/runtime/server/page/render.js | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 7896e910180f..6e2ea4b0b3cd 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -36,6 +36,16 @@ export async function render_response({ ssr, stuff }) { + if (state.prerender) { + if (options.csp.mode === 'nonce') { + throw new Error('Cannot use prerendering if config.kit.csp.mode === "nonce"'); + } + + if (options.template_contains_nonce) { + throw new Error('Cannot use prerendering if page template contains %svelte.nonce%'); + } + } + const stylesheets = new Set(options.manifest._.entry.css); const modulepreloads = new Set(options.manifest._.entry.js); /** @type {Map} */ @@ -128,6 +138,7 @@ export async function render_response({ const inlined_style = Array.from(styles.values()).join('\n'); + // CSP stuff const script_src = options.csp.directives['script-src'] || options.csp.directives['default-src']; const style_src = options.csp.directives['style-src'] || options.csp.directives['default-src']; @@ -137,8 +148,15 @@ export async function render_response({ const needs_styles_csp = style_src && style_src.filter((value) => value !== 'unsafe-inline').length > 0; + const use_hashes = + options.csp.mode === 'hash' || (options.csp.mode === 'auto' && state.prerender); + + const use_nonce = options.csp.mode !== 'hash'; + + const nonce = (use_nonce || options.template_contains_nonce) && generate_nonce(); + // prettier-ignore - const init = ` + const init_app = ` import { start } from ${s(options.prefix + options.manifest._.entry.file)}; start({ target: ${options.target ? `document.querySelector(${s(options.target)})` : 'document.body'}, @@ -160,7 +178,14 @@ export async function render_response({ url: new URL(${s(url.href)}), params: ${devalue(params)} }` : 'null'} - });`; + }); + `; + + const init_service_worker = ` + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('${options.service_worker}'); + } + `; if (state.prerender) { if (maxage) { @@ -200,7 +225,7 @@ export async function render_response({ .join(''); head += ` - `; + `; body += serialized_data .map(({ url, body, json }) => { @@ -229,7 +254,7 @@ export async function render_response({ const assets = options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.'); - const html = options.template({ head, body, assets }); + const html = options.template({ head, body, assets, nonce }); const headers = new Headers({ 'content-type': 'text/html', From cae0b70c22569f45f84d8b86da413da708c7eb45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 22 Jan 2022 07:49:12 -0500 Subject: [PATCH 07/60] generate meta tags last --- .../kit/src/runtime/server/page/render.js | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 6e2ea4b0b3cd..89ae68efd003 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -187,12 +187,6 @@ export async function render_response({ } `; - if (state.prerender) { - if (maxage) { - head += ``; - } - } - if (options.amp) { head += ` @@ -242,12 +236,23 @@ export async function render_response({ if (options.service_worker) { // always include service worker unless it's turned off explicitly head += ` - `; + `; + } + } + + if (state.prerender) { + /** @type {Array<{ key: string, value: string }>} */ + const http_equiv = []; + + if (maxage) { + http_equiv.push({ key: 'cache-control', value: `max-age=${maxage}` }); } + + const tags = http_equiv.map( + ({ key, value }) => `` + ); + + head = tags + head; } const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2); From 32b485f83b50f676c4d058a2d7b17eaa4bde294b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 23 Jan 2022 23:02:31 -0500 Subject: [PATCH 08/60] move CSP logic out into separate (and more testable) class --- packages/kit/src/core/build/build_server.js | 13 +- packages/kit/src/core/dev/plugin.js | 5 +- packages/kit/src/runtime/server/page/csp.js | 210 ++++++++++++++++++ .../kit/src/runtime/server/page/render.js | 107 ++++++--- packages/kit/types/csp.d.ts | 2 +- packages/kit/types/internal.d.ts | 13 +- 6 files changed, 310 insertions(+), 40 deletions(-) create mode 100644 packages/kit/src/runtime/server/page/csp.js diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index de0c337ec0a6..7a8efbeac424 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -11,21 +11,21 @@ import { s } from '../../utils/misc.js'; /** * @param {{ - * cwd: string; * hooks: string; * config: import('types/config').ValidatedConfig; * has_service_worker: boolean; + * template: string; * }} opts * @returns */ -const template = ({ cwd, config, hooks, has_service_worker }) => ` +const app_template = ({ config, hooks, has_service_worker, template }) => ` import root from '__GENERATED__/root.svelte'; import { respond } from '${runtime}/server/index.js'; import { set_paths, assets, base } from '${runtime}/paths.js'; import { set_prerendering } from '${runtime}/env.js'; import * as user_hooks from ${s(hooks)}; -const template = ({ head, body, assets }) => ${s(load_template(cwd, config)) +const template = ({ head, body, assets }) => ${s(template) .replace('%svelte.head%', '" + head + "') .replace('%svelte.body%', '" + body + "') .replace(/%svelte\.assets%/g, '" + assets + "')}; @@ -90,6 +90,7 @@ export class App { router: ${s(config.kit.router)}, target: ${s(config.kit.target)}, template, + template_contains_nonce: ${template.includes('%svelte.nonce%')}, trailing_slash: ${s(config.kit.trailingSlash)} }; } @@ -173,11 +174,11 @@ export async function build_server( // prettier-ignore fs.writeFileSync( input.app, - template({ - cwd, + app_template({ config, hooks: app_relative(hooks_file), - has_service_worker: service_worker_register && !!service_worker_entry_file + has_service_worker: service_worker_register && !!service_worker_entry_file, + template: load_template(cwd, config) }) ); diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index ffa994034776..b27478a64f75 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -216,6 +216,8 @@ export async function create_plugin(config, cwd) { return res.end(err.reason || 'Invalid request body'); } + const template = load_template(cwd, config); + const rendered = await respond( new Request(url.href, { headers: /** @type {Record} */ (req.headers), @@ -261,7 +263,7 @@ export async function create_plugin(config, cwd) { router: config.kit.router, target: config.kit.target, template: ({ head, body, assets }) => { - let rendered = load_template(cwd, config) + let rendered = template .replace('%svelte.head%', () => head) .replace('%svelte.body%', () => body) .replace(/%svelte\.assets%/g, assets); @@ -312,6 +314,7 @@ export async function create_plugin(config, cwd) { return rendered; }, + template_contains_nonce: template.includes('%svelte.nonce%'), trailing_slash: config.kit.trailingSlash } ); diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js new file mode 100644 index 000000000000..8a185408b6b0 --- /dev/null +++ b/packages/kit/src/runtime/server/page/csp.js @@ -0,0 +1,210 @@ +import { webcrypto } from 'crypto'; +import { escape_html_attr } from '../../../utils/escape.js'; + +const array = new Uint8Array(16); + +export function generate_nonce() { + webcrypto.getRandomValues(array); + return base64(array); +} + +/** + * @param {string} contents + * @param {string} algorithm + * @returns + */ +async function generate_hash(contents, algorithm = 'sha-256') { + const bytes = new TextEncoder().encode(contents); + const digest = new Uint8Array(await webcrypto.subtle.digest(algorithm, bytes)); + return base64(digest); +} + +const quoted = new Set([ + 'self', + 'unsafe-eval', + 'unsafe-hashes', + 'unsafe-inline', + 'none', + 'strict-dynamic', + 'report-sample' +]); + +const crypto_pattern = /^(nonce|sha\d\d\d)-/; + +export class Csp { + /** @type {boolean} */ + #use_hashes; + + /** @type {import('types/csp').CspDirectives} */ + #directives; + + /** @type {import('types/csp').Source[]} */ + #script_src; + + /** @type {import('types/csp').Source[]} */ + #style_src; + + /** + * @param {{ + * mode: string, + * directives: import('types/csp').CspDirectives + * }} opts + * @param {boolean} prerender + */ + constructor({ mode, directives }, prerender) { + this.#use_hashes = mode === 'hash' || (mode === 'auto' && prerender); + this.#directives = { ...directives }; + + this.#script_src = []; + this.#style_src = []; + + const effective_script_src = directives['script-src'] || directives['default-src']; + const effective_style_src = directives['style-src'] || directives['default-src']; + + this.script_needs_csp = + effective_script_src && + effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0; + + this.style_needs_csp = + effective_style_src && + effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0; + + this.script_needs_nonce = this.script_needs_csp && !this.#use_hashes; + this.style_needs_nonce = this.style_needs_csp && !this.#use_hashes; + + if (this.script_needs_nonce || this.style_needs_nonce) { + this.nonce = generate_nonce(); + } + } + + // TODO would be great if these methods weren't async + /** @param {string} content */ + async add_script(content) { + if (this.script_needs_csp) { + if (this.#use_hashes) { + this.#script_src.push(`sha256-${await generate_hash(content)}`); + } else if (this.#script_src.length === 0) { + this.#script_src.push(`nonce-${this.nonce}`); + } + } + } + + /** @param {string} content */ + async add_style(content) { + if (this.style_needs_csp) { + if (this.#use_hashes) { + this.#style_src.push(`sha256-${await generate_hash(content)}`); + } else if (this.#style_src.length === 0) { + this.#style_src.push(`nonce-${this.nonce}`); + } + } + } + + /** @param {boolean} [is_meta] */ + get_header(is_meta = false) { + const header = []; + + // due to browser inconsistencies, we can't append sources to default-src + // (specifically, Firefox appears to not ignore nonce-{nonce} directives + // on default-src), so we ensure that script-src and style-src exist + + if (this.#script_src.length > 0) { + if (!this.#directives['script-src']) { + this.#directives['script-src'] = [...(this.#directives['default-src'] || [])]; + } + + this.#directives['script-src'].push(...this.#script_src); + } + + if (this.#style_src.length > 0) { + if (!this.#directives['style-src']) { + this.#directives['style-src'] = [...(this.#directives['default-src'] || [])]; + } + + this.#directives['style-src'].push(...this.#style_src); + } + + for (const key in this.#directives) { + if (is_meta && (key === 'frame-ancestors' || key === 'report-uri' || key === 'sandbox')) { + // these values cannot be used with a tag + // TODO warn? + continue; + } + + // @ts-expect-error gimme a break typescript, `key` is obviously a member of this.#directives + const value = /** @type {string[] | true} */ (this.#directives[key]); + + if (!value) continue; + + const directive = [key]; + if (Array.isArray(value)) { + value.forEach((value) => { + if (quoted.has(value) || crypto_pattern.test(value)) { + directive.push(`'${value}'`); + } else { + directive.push(value); + } + }); + } + + header.push(directive.join(' ')); + } + + return header.join('; '); + } + + get_meta() { + const content = escape_html_attr(this.get_header(true)); + return `> 2]; + result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; + result += chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)]; + result += chars[bytes[i] & 0x3f]; + } + if (i === l + 1) { + // 1 octet yet to write + result += chars[bytes[i - 2] >> 2]; + result += chars[(bytes[i - 2] & 0x03) << 4]; + result += '=='; + } + if (i === l) { + // 2 octets yet to write + result += chars[bytes[i - 2] >> 2]; + result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; + result += chars[(bytes[i - 1] & 0x0f) << 2]; + result += '='; + } + return result; +} diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 89ae68efd003..2bd082599081 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -5,6 +5,7 @@ import { hash } from '../../hash.js'; import { escape_html_attr } from '../../../utils/escape.js'; import { s } from '../../../utils/misc.js'; import { create_prerendering_url_proxy } from './utils.js'; +import { Csp } from './csp.js'; // TODO rename this function/module @@ -138,22 +139,7 @@ export async function render_response({ const inlined_style = Array.from(styles.values()).join('\n'); - // CSP stuff - const script_src = options.csp.directives['script-src'] || options.csp.directives['default-src']; - const style_src = options.csp.directives['style-src'] || options.csp.directives['default-src']; - - const needs_scripts_csp = - script_src && script_src.filter((value) => value !== 'unsafe-inline').length > 0; - - const needs_styles_csp = - style_src && style_src.filter((value) => value !== 'unsafe-inline').length > 0; - - const use_hashes = - options.csp.mode === 'hash' || (options.csp.mode === 'auto' && state.prerender); - - const use_nonce = options.csp.mode !== 'hash'; - - const nonce = (use_nonce || options.template_contains_nonce) && generate_nonce(); + const csp = new Csp(options.csp, !!state.prerender); // prettier-ignore const init_app = ` @@ -205,12 +191,31 @@ export async function render_response({ if (inlined_style) { const attributes = []; if (options.dev) attributes.push(' data-svelte'); + if (csp.style_needs_nonce) attributes.push(` nonce="${csp.nonce}"`); + + await csp.add_style(inlined_style); head += `\n\t${inlined_style}`; } + // prettier-ignore head += Array.from(stylesheets) - .map((dep) => `\n\t`) + .map((dep) => { + const attributes = [ + 'rel="stylesheet"', + `href="${options.prefix + dep}"` + ]; + + if (csp.style_needs_nonce) { + attributes.push(`nonce="${csp.nonce}"`); + } + + if (styles.has(dep)) { + attributes.push('disabled', 'media="(max-width: 0)"'); + } + + return `\n\t` + }) .join(''); if (page_config.router || page_config.hydrate) { @@ -218,14 +223,20 @@ export async function render_response({ .map((dep) => `\n\t`) .join(''); - head += ` - `; + const attributes = ['type="module"']; + await csp.add_script(init_app); + + if (csp.script_needs_nonce) { + attributes.push(`nonce="${csp.nonce}"`); + } + + head += ``; + + // prettier-ignore body += serialized_data .map(({ url, body, json }) => { - let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr( - url - )}`; + let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(url)}`; if (body) attributes += ` data-body="${hash(body)}"`; return ``; @@ -235,31 +246,35 @@ export async function render_response({ if (options.service_worker) { // always include service worker unless it's turned off explicitly + await csp.add_script(init_service_worker); + head += ` - `; + ${init_service_worker}`; } } if (state.prerender) { - /** @type {Array<{ key: string, value: string }>} */ const http_equiv = []; - if (maxage) { - http_equiv.push({ key: 'cache-control', value: `max-age=${maxage}` }); + const csp_headers = csp.get_meta(); + if (csp_headers) { + http_equiv.push(csp_headers); } - const tags = http_equiv.map( - ({ key, value }) => `` - ); + if (maxage) { + http_equiv.push(``); + } - head = tags + head; + if (http_equiv.length > 0) { + head = http_equiv.join('\n') + head; + } } const segments = url.pathname.slice(options.paths.base.length).split('/').slice(2); const assets = options.paths.assets || (segments.length > 0 ? segments.map(() => '..').join('/') : '.'); - const html = options.template({ head, body, assets, nonce }); + const html = options.template({ head, body, assets, nonce: /** @type {string} */ (csp.nonce) }); const headers = new Headers({ 'content-type': 'text/html', @@ -274,12 +289,42 @@ export async function render_response({ headers.set('permissions-policy', 'interest-cohort=()'); } + if (!state.prerender) { + headers.set('content-security-policy', csp.get_header()); + } + return new Response(html, { status, headers }); } +/** + * + * @param {string} type + * @param {Record} attributes + * @param {string} [content] + */ +function element(type, attributes, content) { + let result = `<${type}`; + + for (const key in attributes) { + const value = attributes[key]; + result += ' ' + key; + if (value !== true) { + result += escape_html_attr(value); + } + } + + if (content) { + result += `>${content}`; + } else { + result += '/>'; + } + + return result; +} + /** * @param {any} data * @param {(error: Error) => void} [fail] diff --git a/packages/kit/types/csp.d.ts b/packages/kit/types/csp.d.ts index 5b390dd12a9f..09c8b101ef9b 100644 --- a/packages/kit/types/csp.d.ts +++ b/packages/kit/types/csp.d.ts @@ -37,7 +37,7 @@ type CryptoSource = `${'nonce' | 'sha256' | 'sha384' | 'sha512'}-${string}`; type BaseSource = 'self' | 'unsafe-eval' | 'unsafe-hashes' | 'unsafe-inline' | 'none'; -type Source = HostSource | SchemeSource | CryptoSource | BaseSource; +export type Source = HostSource | SchemeSource | CryptoSource | BaseSource; type Sources = Source[]; type ActionSource = 'strict-dynamic' | 'report-sample'; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 7163d61424f0..8412a7a72dfe 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -136,7 +136,18 @@ export interface SSRRenderOptions { router: boolean; service_worker?: string; target: string; - template({ head, body, assets }: { head: string; body: string; assets: string }): string; + template({ + head, + body, + assets, + nonce + }: { + head: string; + body: string; + assets: string; + nonce: string; + }): string; + template_contains_nonce: boolean; trailing_slash: TrailingSlash; } From e86d2f0a8f1fa02b3e11440d0bb605431fba249b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Jan 2022 21:33:35 -0500 Subject: [PATCH 09/60] fixes --- packages/kit/src/core/build/build_server.js | 2 +- packages/kit/src/runtime/server/page/render.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js index c8d24d3a49e9..5f0954e49115 100644 --- a/packages/kit/src/core/build/build_server.js +++ b/packages/kit/src/core/build/build_server.js @@ -28,7 +28,7 @@ import * as user_hooks from ${s(hooks)}; const template = ({ head, body, assets, nonce }) => ${s(template) .replace('%svelte.head%', '" + head + "') .replace('%svelte.body%', '" + body + "') - .replace(/%svelte\.assets%/g, '" + assets + "')} + .replace(/%svelte\.assets%/g, '" + assets + "') .replace(/%svelte\.nonce%/g, '" + nonce + "')}; let read = null; diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 2bd082599081..7047857978dc 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -290,7 +290,10 @@ export async function render_response({ } if (!state.prerender) { - headers.set('content-security-policy', csp.get_header()); + const csp_header = csp.get_header(); + if (csp_header) { + headers.set('content-security-policy', csp_header); + } } return new Response(html, { From c533bf80ea524ff09b015db48303ff2269968089 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Jan 2022 21:39:00 -0500 Subject: [PATCH 10/60] fix --- packages/kit/src/runtime/server/page/render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 7047857978dc..6be03a0a56bf 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -214,7 +214,7 @@ export async function render_response({ attributes.push('disabled', 'media="(max-width: 0)"'); } - return `\n\t` + return `\n\t` }) .join(''); From a71d0d6c6aeb586a711b4350ec9e5de3361d32b2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Jan 2022 21:43:32 -0500 Subject: [PATCH 11/60] lint --- packages/kit/src/runtime/server/page/csp.js | 11 ++++++-- .../kit/src/runtime/server/page/render.js | 28 +------------------ 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 8a185408b6b0..95b50d723726 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -184,21 +184,25 @@ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' /** @param {Uint8Array} bytes */ function base64(bytes) { - let result = '', - i, - l = bytes.length; + const l = bytes.length; + + let result = ''; + let i; + for (i = 2; i < l; i += 3) { result += chars[bytes[i - 2] >> 2]; result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; result += chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)]; result += chars[bytes[i] & 0x3f]; } + if (i === l + 1) { // 1 octet yet to write result += chars[bytes[i - 2] >> 2]; result += chars[(bytes[i - 2] & 0x03) << 4]; result += '=='; } + if (i === l) { // 2 octets yet to write result += chars[bytes[i - 2] >> 2]; @@ -206,5 +210,6 @@ function base64(bytes) { result += chars[(bytes[i - 1] & 0x0f) << 2]; result += '='; } + return result; } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 6be03a0a56bf..39a13eec5534 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -214,7 +214,7 @@ export async function render_response({ attributes.push('disabled', 'media="(max-width: 0)"'); } - return `\n\t` + return `\n\t`; }) .join(''); @@ -302,32 +302,6 @@ export async function render_response({ }); } -/** - * - * @param {string} type - * @param {Record} attributes - * @param {string} [content] - */ -function element(type, attributes, content) { - let result = `<${type}`; - - for (const key in attributes) { - const value = attributes[key]; - result += ' ' + key; - if (value !== true) { - result += escape_html_attr(value); - } - } - - if (content) { - result += `>${content}`; - } else { - result += '/>'; - } - - return result; -} - /** * @param {any} data * @param {(error: Error) => void} [fail] From 4d519c371be698b7e105e3c2daf96e08277f1f29 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Jan 2022 21:54:13 -0500 Subject: [PATCH 12/60] add test to show CSP headers are working --- .../options/source/pages/csp/index.svelte | 7 ++++++ packages/kit/test/apps/options/test/test.js | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 packages/kit/test/apps/options/source/pages/csp/index.svelte diff --git a/packages/kit/test/apps/options/source/pages/csp/index.svelte b/packages/kit/test/apps/options/source/pages/csp/index.svelte new file mode 100644 index 000000000000..8b0c1ae6ba35 --- /dev/null +++ b/packages/kit/test/apps/options/source/pages/csp/index.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index d2d7b818fa3f..09221e5b3a6d 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { start_server } from '../../../utils.js'; import { test } from '../../../utils.js'; /** @typedef {import('@playwright/test').Response} Response */ @@ -78,6 +79,28 @@ test.describe.parallel('base path', () => { }); }); +test.describe.parallel('CSP', () => { + test('blocks script from external site', async ({ page }) => { + const { server, port } = await start_server((req, res) => { + if (req.url === '/blocked.js') { + res.writeHead(200, { + 'content-type': 'text/javascript' + }); + + res.end('window.pwned = true'); + } else { + res.writeHead(404).end('not found'); + } + }); + + await page.goto(`/path-base/csp?port=${port}`); + + expect(await page.evaluate('window.pwned')).toBe(undefined); + + server.close(); + }); +}); + test.describe.parallel('Custom extensions', () => { test('works with arbitrary extensions', async ({ page }) => { await page.goto('/path-base/custom-extensions/'); From 750794e9c33cfc71b488697484f8b6cacaf59f38 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Jan 2022 22:00:31 -0500 Subject: [PATCH 13/60] test for tags --- .../prerendering/options/svelte.config.js | 8 ++++++++ .../test/prerendering/options/test/test.js | 9 +++++++++ pnpm-lock.yaml | 20 +++++++++++++++++++ pnpm-workspace.yaml | 1 + 4 files changed, 38 insertions(+) diff --git a/packages/kit/test/prerendering/options/svelte.config.js b/packages/kit/test/prerendering/options/svelte.config.js index 55f288d4a2a3..ac5499a0da49 100644 --- a/packages/kit/test/prerendering/options/svelte.config.js +++ b/packages/kit/test/prerendering/options/svelte.config.js @@ -5,10 +5,18 @@ import adapter from '../../../../adapter-static/index.js'; const config = { kit: { adapter: adapter(), + + csp: { + directives: { + 'script-src': ['self'] + } + }, + paths: { base: '/path-base', assets: 'https://cdn.example.com/stuff' }, + vite: { build: { minify: false diff --git a/packages/kit/test/prerendering/options/test/test.js b/packages/kit/test/prerendering/options/test/test.js index 6aaaef278c5d..260d1f6f7532 100644 --- a/packages/kit/test/prerendering/options/test/test.js +++ b/packages/kit/test/prerendering/options/test/test.js @@ -20,4 +20,13 @@ test('prerenders nested /path-base', () => { assert.ok(content.includes('http://sveltekit-prerender/path-base/nested')); }); +test('adds CSP headers via meta tag', () => { + const content = read('index.html'); + assert.ok( + content.includes( + ` Date: Tue, 25 Jan 2022 09:39:43 -0500 Subject: [PATCH 17/60] add relevant subset of sjcl --- .../kit/src/runtime/server/page/crypto.js | 81 ++ .../src/runtime/server/page/crypto.spec.js | 17 + packages/kit/src/runtime/server/page/sjcl.js | 802 ++++++++++++++++++ 3 files changed, 900 insertions(+) create mode 100644 packages/kit/src/runtime/server/page/crypto.js create mode 100644 packages/kit/src/runtime/server/page/crypto.spec.js create mode 100644 packages/kit/src/runtime/server/page/sjcl.js diff --git a/packages/kit/src/runtime/server/page/crypto.js b/packages/kit/src/runtime/server/page/crypto.js new file mode 100644 index 000000000000..450cc310a808 --- /dev/null +++ b/packages/kit/src/runtime/server/page/crypto.js @@ -0,0 +1,81 @@ +import { sjcl } from './sjcl.js'; + +// adapted from https://bitwiseshiftleft.github.io/sjcl/, +// modified and redistributed under BSD license + +/** @param {string} text */ +export function sha256(text) { + const hashed = new Uint32Array(sjcl.hash.sha256.hash(text)); + const buffer = hashed.buffer; + const uint8array = new Uint8Array(buffer); + + // bitArray is big endian, uint32array is little endian, so we need to do this: + for (let i = 0; i < uint8array.length; i += 4) { + const a = uint8array[i + 0]; + const b = uint8array[i + 1]; + const c = uint8array[i + 2]; + const d = uint8array[i + 3]; + + uint8array[i + 0] = d; + uint8array[i + 1] = c; + uint8array[i + 2] = b; + uint8array[i + 3] = a; + } + + return base64(uint8array); +} + +/* + Based on https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 + + MIT License + Copyright (c) 2020 Egor Nepomnyaschih + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); + +/** @param {Uint8Array} bytes */ +function base64(bytes) { + const l = bytes.length; + + let result = ''; + let i; + + for (i = 2; i < l; i += 3) { + result += chars[bytes[i - 2] >> 2]; + result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; + result += chars[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)]; + result += chars[bytes[i] & 0x3f]; + } + + if (i === l + 1) { + // 1 octet yet to write + result += chars[bytes[i - 2] >> 2]; + result += chars[(bytes[i - 2] & 0x03) << 4]; + result += '=='; + } + + if (i === l) { + // 2 octets yet to write + result += chars[bytes[i - 2] >> 2]; + result += chars[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; + result += chars[(bytes[i - 1] & 0x0f) << 2]; + result += '='; + } + + return result; +} diff --git a/packages/kit/src/runtime/server/page/crypto.spec.js b/packages/kit/src/runtime/server/page/crypto.spec.js new file mode 100644 index 000000000000..d620f39b514a --- /dev/null +++ b/packages/kit/src/runtime/server/page/crypto.spec.js @@ -0,0 +1,17 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import crypto from 'crypto'; +import { sha256 } from './crypto.js'; + +const inputs = ['the quick brown fox jumps over the lazy dog', '工欲善其事,必先利其器']; + +inputs.forEach((input) => { + test(input, () => { + const expected_bytes = crypto.createHash('sha256').update(input, 'utf-8').digest(); + const expected = expected_bytes.toString('base64'); + + assert.equal(sha256(input), expected); + }); +}); + +test.run(); diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js new file mode 100644 index 000000000000..ee9d9dc90d63 --- /dev/null +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -0,0 +1,802 @@ +export const sjcl = { + hash: {}, + codec: {} +}; + +/** @fileOverview Bit array codec implementations. + + * + + * @author Emily Stark + + * @author Mike Hamburg + + * @author Dan Boneh + + */ + +/** + + * UTF-8 strings + + * @namespace + + */ + +sjcl.codec.utf8String = { + /** Convert from a bitArray to a UTF-8 string. */ + + fromBits: function (arr) { + var out = '', + bl = sjcl.bitArray.bitLength(arr), + i, + tmp; + + for (i = 0; i < bl / 8; i++) { + if ((i & 3) === 0) { + tmp = arr[i / 4]; + } + + out += String.fromCharCode(tmp >>> 24); + + tmp <<= 8; + } + + return decodeURIComponent(escape(out)); + }, + + /** Convert from a UTF-8 string to a bitArray. */ + + toBits: function (str) { + str = unescape(encodeURIComponent(str)); + + var out = [], + i, + tmp = 0; + + for (i = 0; i < str.length; i++) { + tmp = (tmp << 8) | str.charCodeAt(i); + + if ((i & 3) === 3) { + out.push(tmp); + + tmp = 0; + } + } + + if (i & 3) { + out.push(sjcl.bitArray.partial(8 * (i & 3), tmp)); + } + + return out; + } +}; + +/** @fileOverview Arrays of bits, encoded as arrays of Numbers. + + * + + * @author Emily Stark + + * @author Mike Hamburg + + * @author Dan Boneh + + */ + +/** + + * Arrays of bits, encoded as arrays of Numbers. + + * @namespace + + * @description + + *

+ + * These objects are the currency accepted by SJCL's crypto functions. + + *

+ + * + + *

+ + * Most of our crypto primitives operate on arrays of 4-byte words internally, + + * but many of them can take arguments that are not a multiple of 4 bytes. + + * This library encodes arrays of bits (whose size need not be a multiple of 8 + + * bits) as arrays of 32-bit words. The bits are packed, big-endian, into an + + * array of words, 32 bits at a time. Since the words are double-precision + + * floating point numbers, they fit some extra data. We use this (in a private, + + * possibly-changing manner) to encode the number of bits actually present + + * in the last word of the array. + + *

+ + * + + *

+ + * Because bitwise ops clear this out-of-band data, these arrays can be passed + + * to ciphers like AES which want arrays of words. + + *

+ + */ + +sjcl.bitArray = { + /** + + * Array slices in units of bits. + + * @param {bitArray} a The array to slice. + + * @param {Number} bstart The offset to the start of the slice, in bits. + + * @param {Number} bend The offset to the end of the slice, in bits. If this is undefined, + + * slice until the end of the array. + + * @return {bitArray} The requested slice. + + */ + + bitSlice: function (a, bstart, bend) { + a = sjcl.bitArray._shiftRight(a.slice(bstart / 32), 32 - (bstart & 31)).slice(1); + + return bend === undefined ? a : sjcl.bitArray.clamp(a, bend - bstart); + }, + + /** + + * Extract a number packed into a bit array. + + * @param {bitArray} a The array to slice. + + * @param {Number} bstart The offset to the start of the slice, in bits. + + * @param {Number} blength The length of the number to extract. + + * @return {Number} The requested slice. + + */ + + extract: function (a, bstart, blength) { + // FIXME: this Math.floor is not necessary at all, but for some reason + + // seems to suppress a bug in the Chromium JIT. + + var x, + sh = Math.floor((-bstart - blength) & 31); + + if (((bstart + blength - 1) ^ bstart) & -32) { + // it crosses a boundary + + x = (a[(bstart / 32) | 0] << (32 - sh)) ^ (a[(bstart / 32 + 1) | 0] >>> sh); + } else { + // within a single word + + x = a[(bstart / 32) | 0] >>> sh; + } + + return x & ((1 << blength) - 1); + }, + + /** + + * Concatenate two bit arrays. + + * @param {bitArray} a1 The first array. + + * @param {bitArray} a2 The second array. + + * @return {bitArray} The concatenation of a1 and a2. + + */ + + concat: function (a1, a2) { + if (a1.length === 0 || a2.length === 0) { + return a1.concat(a2); + } + + var last = a1[a1.length - 1], + shift = sjcl.bitArray.getPartial(last); + + if (shift === 32) { + return a1.concat(a2); + } else { + return sjcl.bitArray._shiftRight(a2, shift, last | 0, a1.slice(0, a1.length - 1)); + } + }, + + /** + + * Find the length of an array of bits. + + * @param {bitArray} a The array. + + * @return {Number} The length of a, in bits. + + */ + + bitLength: function (a) { + var l = a.length, + x; + + if (l === 0) { + return 0; + } + + x = a[l - 1]; + + return (l - 1) * 32 + sjcl.bitArray.getPartial(x); + }, + + /** + + * Truncate an array. + + * @param {bitArray} a The array. + + * @param {Number} len The length to truncate to, in bits. + + * @return {bitArray} A new array, truncated to len bits. + + */ + + clamp: function (a, len) { + if (a.length * 32 < len) { + return a; + } + + a = a.slice(0, Math.ceil(len / 32)); + + var l = a.length; + + len = len & 31; + + if (l > 0 && len) { + a[l - 1] = sjcl.bitArray.partial(len, a[l - 1] & (0x80000000 >> (len - 1)), 1); + } + + return a; + }, + + /** + + * Make a partial word for a bit array. + + * @param {Number} len The number of bits in the word. + + * @param {Number} x The bits. + + * @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side. + + * @return {Number} The partial word. + + */ + + partial: function (len, x, _end) { + if (len === 32) { + return x; + } + + return (_end ? x | 0 : x << (32 - len)) + len * 0x10000000000; + }, + + /** + + * Get the number of bits used by a partial word. + + * @param {Number} x The partial word. + + * @return {Number} The number of bits used by the partial word. + + */ + + getPartial: function (x) { + return Math.round(x / 0x10000000000) || 32; + }, + + /** + + * Compare two arrays for equality in a predictable amount of time. + + * @param {bitArray} a The first array. + + * @param {bitArray} b The second array. + + * @return {boolean} true if a == b; false otherwise. + + */ + + equal: function (a, b) { + if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) { + return false; + } + + var x = 0, + i; + + for (i = 0; i < a.length; i++) { + x |= a[i] ^ b[i]; + } + + return x === 0; + }, + + /** Shift an array right. + + * @param {bitArray} a The array to shift. + + * @param {Number} shift The number of bits to shift. + + * @param {Number} [carry=0] A byte to carry in + + * @param {bitArray} [out=[]] An array to prepend to the output. + + * @private + + */ + + _shiftRight: function (a, shift, carry, out) { + var i, + last2 = 0, + shift2; + + if (out === undefined) { + out = []; + } + + for (; shift >= 32; shift -= 32) { + out.push(carry); + + carry = 0; + } + + if (shift === 0) { + return out.concat(a); + } + + for (i = 0; i < a.length; i++) { + out.push(carry | (a[i] >>> shift)); + + carry = a[i] << (32 - shift); + } + + last2 = a.length ? a[a.length - 1] : 0; + + shift2 = sjcl.bitArray.getPartial(last2); + + out.push( + sjcl.bitArray.partial((shift + shift2) & 31, shift + shift2 > 32 ? carry : out.pop(), 1) + ); + + return out; + }, + + /** xor a block of 4 words together. + + * @private + + */ + + _xor4: function (x, y) { + return [x[0] ^ y[0], x[1] ^ y[1], x[2] ^ y[2], x[3] ^ y[3]]; + }, + + /** byteswap a word array inplace. + + * (does not handle partial words) + + * @param {sjcl.bitArray} a word array + + * @return {sjcl.bitArray} byteswapped array + + */ + + byteswapM: function (a) { + var i, + v, + m = 0xff00; + + for (i = 0; i < a.length; ++i) { + v = a[i]; + + a[i] = (v >>> 24) | ((v >>> 8) & m) | ((v & m) << 8) | (v << 24); + } + + return a; + } +}; + +/** @fileOverview Javascript SHA-256 implementation. + + * + + * An older version of this implementation is available in the public + + * domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh, + + * Stanford University 2008-2010 and BSD-licensed for liability + + * reasons. + + * + + * Special thanks to Aldo Cortesi for pointing out several bugs in + + * this code. + + * + + * @author Emily Stark + + * @author Mike Hamburg + + * @author Dan Boneh + + */ + +/** + + * Context for a SHA-256 operation in progress. + + * @constructor + + */ + +sjcl.hash.sha256 = function (hash) { + if (!this._key[0]) { + this._precompute(); + } + + if (hash) { + this._h = hash._h.slice(0); + + this._buffer = hash._buffer.slice(0); + + this._length = hash._length; + } else { + this.reset(); + } +}; + +/** + + * Hash a string or an array of words. + + * @static + + * @param {bitArray|String} data the data to hash. + + * @return {bitArray} The hash value, an array of 16 big-endian words. + + */ + +sjcl.hash.sha256.hash = function (data) { + return new sjcl.hash.sha256().update(data).finalize(); +}; + +sjcl.hash.sha256.prototype = { + /** + + * The hash's block size, in bits. + + * @constant + + */ + + blockSize: 512, + + /** + + * Reset the hash state. + + * @return this + + */ + + reset: function () { + this._h = this._init.slice(0); + + this._buffer = []; + + this._length = 0; + + return this; + }, + + /** + + * Input several words to the hash. + + * @param {bitArray|String} data the data to hash. + + * @return this + + */ + + update: function (data) { + if (typeof data === 'string') { + data = sjcl.codec.utf8String.toBits(data); + } + + var i, + b = (this._buffer = sjcl.bitArray.concat(this._buffer, data)), + ol = this._length, + nl = (this._length = ol + sjcl.bitArray.bitLength(data)); + + if (nl > 9007199254740991) { + throw new sjcl.exception.invalid('Cannot hash more than 2^53 - 1 bits'); + } + + if (typeof Uint32Array !== 'undefined') { + var c = new Uint32Array(b); + + var j = 0; + + for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { + this._block(c.subarray(16 * j, 16 * (j + 1))); + + j += 1; + } + + b.splice(0, 16 * j); + } else { + for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { + this._block(b.splice(0, 16)); + } + } + + return this; + }, + + /** + + * Complete hashing and output the hash value. + + * @return {bitArray} The hash value, an array of 8 big-endian words. + + */ + + finalize: function () { + var i, + b = this._buffer, + h = this._h; + + // Round out and push the buffer + + b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1, 1)]); + + // Round out the buffer to a multiple of 16 words, less the 2 length words. + + for (i = b.length + 2; i & 15; i++) { + b.push(0); + } + + // append the length + + b.push(Math.floor(this._length / 0x100000000)); + + b.push(this._length | 0); + + while (b.length) { + this._block(b.splice(0, 16)); + } + + this.reset(); + + return h; + }, + + /** + + * The SHA-256 initialization vector, to be precomputed. + + * @private + + */ + + _init: [], + + /* + + _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], + + */ + + /** + + * The SHA-256 hash key, to be precomputed. + + * @private + + */ + + _key: [], + + /* + + _key: + + [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], + + */ + + /** + + * Function to precompute _init and _key. + + * @private + + */ + + _precompute: function () { + var i = 0, + prime = 2, + factor, + isPrime; + + function frac(x) { + return ((x - Math.floor(x)) * 0x100000000) | 0; + } + + for (; i < 64; prime++) { + isPrime = true; + + for (factor = 2; factor * factor <= prime; factor++) { + if (prime % factor === 0) { + isPrime = false; + + break; + } + } + + if (isPrime) { + if (i < 8) { + this._init[i] = frac(Math.pow(prime, 1 / 2)); + } + + this._key[i] = frac(Math.pow(prime, 1 / 3)); + + i++; + } + } + }, + + /** + + * Perform one cycle of SHA-256. + + * @param {Uint32Array|bitArray} w one block of words. + + * @private + + */ + + _block: function (w) { + var i, + tmp, + a, + b, + h = this._h, + k = this._key, + h0 = h[0], + h1 = h[1], + h2 = h[2], + h3 = h[3], + h4 = h[4], + h5 = h[5], + h6 = h[6], + h7 = h[7]; + + /* Rationale for placement of |0 : + + * If a value can overflow is original 32 bits by a factor of more than a few + + * million (2^23 ish), there is a possibility that it might overflow the + + * 53-bit mantissa and lose precision. + + * + + * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that + + * propagates around the loop, and on the hash state h[]. I don't believe + + * that the clamps on h4 and on h0 are strictly necessary, but it's close + + * (for h4 anyway), and better safe than sorry. + + * + + * The clamps on h[] are necessary for the output to be correct even in the + + * common case and for short inputs. + + */ + + for (i = 0; i < 64; i++) { + // load up the input word for this round + + if (i < 16) { + tmp = w[i]; + } else { + a = w[(i + 1) & 15]; + + b = w[(i + 14) & 15]; + + tmp = w[i & 15] = + (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + + ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + + w[i & 15] + + w[(i + 9) & 15]) | + 0; + } + + tmp = + tmp + + h7 + + ((h4 >>> 6) ^ (h4 >>> 11) ^ (h4 >>> 25) ^ (h4 << 26) ^ (h4 << 21) ^ (h4 << 7)) + + (h6 ^ (h4 & (h5 ^ h6))) + + k[i]; // | 0; + + // shift register + + h7 = h6; + h6 = h5; + h5 = h4; + + h4 = (h3 + tmp) | 0; + + h3 = h2; + h2 = h1; + h1 = h0; + + h0 = + (tmp + + ((h1 & h2) ^ (h3 & (h1 ^ h2))) + + ((h1 >>> 2) ^ (h1 >>> 13) ^ (h1 >>> 22) ^ (h1 << 30) ^ (h1 << 19) ^ (h1 << 10))) | + 0; + } + + h[0] = (h[0] + h0) | 0; + + h[1] = (h[1] + h1) | 0; + + h[2] = (h[2] + h2) | 0; + + h[3] = (h[3] + h3) | 0; + + h[4] = (h[4] + h4) | 0; + + h[5] = (h[5] + h5) | 0; + + h[6] = (h[6] + h6) | 0; + + h[7] = (h[7] + h7) | 0; + } +}; From 003b119b6fa8b80002e2e09d8558516646d50bf3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:12:18 -0500 Subject: [PATCH 18/60] start tidying up --- packages/kit/src/runtime/server/page/sjcl.js | 319 +++---------------- 1 file changed, 49 insertions(+), 270 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index ee9d9dc90d63..c45d73e24f1e 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -1,189 +1,108 @@ export const sjcl = { - hash: {}, - codec: {} + hash: {} }; -/** @fileOverview Bit array codec implementations. +/** @param {Uint32Array} uint32array' */ +function swap_endianness(uint32array) { + const uint8array = new Uint8Array(uint32array.buffer); - * - - * @author Emily Stark - - * @author Mike Hamburg - - * @author Dan Boneh + for (let i = 0; i < uint8array.length; i += 4) { + const a = uint8array[i + 0]; + const b = uint8array[i + 1]; + const c = uint8array[i + 2]; + const d = uint8array[i + 3]; - */ - -/** - - * UTF-8 strings - - * @namespace - - */ - -sjcl.codec.utf8String = { - /** Convert from a bitArray to a UTF-8 string. */ - - fromBits: function (arr) { - var out = '', - bl = sjcl.bitArray.bitLength(arr), - i, - tmp; - - for (i = 0; i < bl / 8; i++) { - if ((i & 3) === 0) { - tmp = arr[i / 4]; - } + uint8array[i + 0] = d; + uint8array[i + 1] = c; + uint8array[i + 2] = b; + uint8array[i + 3] = a; + } - out += String.fromCharCode(tmp >>> 24); + return uint32array; +} - tmp <<= 8; - } +/** @param {string} str */ +function toBits(str) { + str = unescape(encodeURIComponent(str)); - return decodeURIComponent(escape(out)); - }, + var out = [], + i, + tmp = 0; - /** Convert from a UTF-8 string to a bitArray. */ + for (i = 0; i < str.length; i++) { + tmp = (tmp << 8) | str.charCodeAt(i); - toBits: function (str) { - str = unescape(encodeURIComponent(str)); + if ((i & 3) === 3) { + out.push(tmp); - var out = [], - i, tmp = 0; - - for (i = 0; i < str.length; i++) { - tmp = (tmp << 8) | str.charCodeAt(i); - - if ((i & 3) === 3) { - out.push(tmp); - - tmp = 0; - } } - - if (i & 3) { - out.push(sjcl.bitArray.partial(8 * (i & 3), tmp)); - } - - return out; } -}; - -/** @fileOverview Arrays of bits, encoded as arrays of Numbers. - - * - - * @author Emily Stark - * @author Mike Hamburg + if (i & 3) { + out.push(sjcl.bitArray.partial(8 * (i & 3), tmp)); + } - * @author Dan Boneh - - */ + return out; +} /** - * Arrays of bits, encoded as arrays of Numbers. - * @namespace - * @description - *

- * These objects are the currency accepted by SJCL's crypto functions. - *

- * - *

- * Most of our crypto primitives operate on arrays of 4-byte words internally, - * but many of them can take arguments that are not a multiple of 4 bytes. - * This library encodes arrays of bits (whose size need not be a multiple of 8 - * bits) as arrays of 32-bit words. The bits are packed, big-endian, into an - * array of words, 32 bits at a time. Since the words are double-precision - * floating point numbers, they fit some extra data. We use this (in a private, - * possibly-changing manner) to encode the number of bits actually present - * in the last word of the array. - *

- * - *

- * Because bitwise ops clear this out-of-band data, these arrays can be passed - * to ciphers like AES which want arrays of words. - *

- */ sjcl.bitArray = { /** - * Array slices in units of bits. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} bend The offset to the end of the slice, in bits. If this is undefined, - * slice until the end of the array. - * @return {bitArray} The requested slice. - */ - bitSlice: function (a, bstart, bend) { a = sjcl.bitArray._shiftRight(a.slice(bstart / 32), 32 - (bstart & 31)).slice(1); - return bend === undefined ? a : sjcl.bitArray.clamp(a, bend - bstart); }, /** - * Extract a number packed into a bit array. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} blength The length of the number to extract. - * @return {Number} The requested slice. - */ - extract: function (a, bstart, blength) { // FIXME: this Math.floor is not necessary at all, but for some reason - // seems to suppress a bug in the Chromium JIT. - var x, sh = Math.floor((-bstart - blength) & 31); if (((bstart + blength - 1) ^ bstart) & -32) { // it crosses a boundary - x = (a[(bstart / 32) | 0] << (32 - sh)) ^ (a[(bstart / 32 + 1) | 0] >>> sh); } else { // within a single word - x = a[(bstart / 32) | 0] >>> sh; } @@ -191,17 +110,11 @@ sjcl.bitArray = { }, /** - * Concatenate two bit arrays. - * @param {bitArray} a1 The first array. - * @param {bitArray} a2 The second array. - * @return {bitArray} The concatenation of a1 and a2. - */ - concat: function (a1, a2) { if (a1.length === 0 || a2.length === 0) { return a1.concat(a2); @@ -218,15 +131,10 @@ sjcl.bitArray = { }, /** - * Find the length of an array of bits. - * @param {bitArray} a The array. - * @return {Number} The length of a, in bits. - */ - bitLength: function (a) { var l = a.length, x; @@ -241,17 +149,11 @@ sjcl.bitArray = { }, /** - * Truncate an array. - * @param {bitArray} a The array. - * @param {Number} len The length to truncate to, in bits. - * @return {bitArray} A new array, truncated to len bits. - */ - clamp: function (a, len) { if (a.length * 32 < len) { return a; @@ -271,19 +173,12 @@ sjcl.bitArray = { }, /** - * Make a partial word for a bit array. - * @param {Number} len The number of bits in the word. - * @param {Number} x The bits. - * @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side. - * @return {Number} The partial word. - */ - partial: function (len, x, _end) { if (len === 32) { return x; @@ -293,29 +188,19 @@ sjcl.bitArray = { }, /** - * Get the number of bits used by a partial word. - * @param {Number} x The partial word. - * @return {Number} The number of bits used by the partial word. - */ - getPartial: function (x) { return Math.round(x / 0x10000000000) || 32; }, /** - * Compare two arrays for equality in a predictable amount of time. - * @param {bitArray} a The first array. - * @param {bitArray} b The second array. - * @return {boolean} true if a == b; false otherwise. - */ equal: function (a, b) { @@ -334,19 +219,12 @@ sjcl.bitArray = { }, /** Shift an array right. - * @param {bitArray} a The array to shift. - * @param {Number} shift The number of bits to shift. - * @param {Number} [carry=0] A byte to carry in - * @param {bitArray} [out=[]] An array to prepend to the output. - * @private - */ - _shiftRight: function (a, shift, carry, out) { var i, last2 = 0, @@ -384,25 +262,17 @@ sjcl.bitArray = { }, /** xor a block of 4 words together. - * @private - */ - _xor4: function (x, y) { return [x[0] ^ y[0], x[1] ^ y[1], x[2] ^ y[2], x[3] ^ y[3]]; }, /** byteswap a word array inplace. - * (does not handle partial words) - * @param {sjcl.bitArray} a word array - * @return {sjcl.bitArray} byteswapped array - */ - byteswapM: function (a) { var i, v, @@ -419,39 +289,22 @@ sjcl.bitArray = { }; /** @fileOverview Javascript SHA-256 implementation. - * - * An older version of this implementation is available in the public - * domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh, - * Stanford University 2008-2010 and BSD-licensed for liability - * reasons. - * - * Special thanks to Aldo Cortesi for pointing out several bugs in - * this code. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - /** - * Context for a SHA-256 operation in progress. - * @constructor - */ sjcl.hash.sha256 = function (hash) { @@ -471,63 +324,41 @@ sjcl.hash.sha256 = function (hash) { }; /** - - * Hash a string or an array of words. - - * @static - - * @param {bitArray|String} data the data to hash. - - * @return {bitArray} The hash value, an array of 16 big-endian words. - - */ - + * Hash a string or an array of words. + * @static + * @param {bitArray|String} data the data to hash. + * @return {bitArray} The hash value, an array of 16 big-endian words. + */ sjcl.hash.sha256.hash = function (data) { return new sjcl.hash.sha256().update(data).finalize(); }; sjcl.hash.sha256.prototype = { /** - * The hash's block size, in bits. - * @constant - */ - blockSize: 512, /** - * Reset the hash state. - * @return this - */ - reset: function () { this._h = this._init.slice(0); - this._buffer = []; - this._length = 0; - return this; }, /** - * Input several words to the hash. - * @param {bitArray|String} data the data to hash. - * @return this - */ - update: function (data) { if (typeof data === 'string') { - data = sjcl.codec.utf8String.toBits(data); + data = toBits(data); } var i, @@ -561,13 +392,9 @@ sjcl.hash.sha256.prototype = { }, /** - * Complete hashing and output the hash value. - * @return {bitArray} The hash value, an array of 8 big-endian words. - */ - finalize: function () { var i, b = this._buffer, @@ -599,61 +426,37 @@ sjcl.hash.sha256.prototype = { }, /** - * The SHA-256 initialization vector, to be precomputed. - * @private - */ - _init: [], /* - _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], - */ /** - * The SHA-256 hash key, to be precomputed. - * @private - */ - _key: [], /* - _key: - [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], - */ /** - * Function to precompute _init and _key. - * @private - */ - _precompute: function () { var i = 0, prime = 2, @@ -688,15 +491,10 @@ sjcl.hash.sha256.prototype = { }, /** - * Perform one cycle of SHA-256. - * @param {Uint32Array|bitArray} w one block of words. - * @private - */ - _block: function (w) { var i, tmp, @@ -714,30 +512,18 @@ sjcl.hash.sha256.prototype = { h7 = h[7]; /* Rationale for placement of |0 : - - * If a value can overflow is original 32 bits by a factor of more than a few - - * million (2^23 ish), there is a possibility that it might overflow the - - * 53-bit mantissa and lose precision. - - * - - * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that - - * propagates around the loop, and on the hash state h[]. I don't believe - - * that the clamps on h4 and on h0 are strictly necessary, but it's close - - * (for h4 anyway), and better safe than sorry. - - * - - * The clamps on h[] are necessary for the output to be correct even in the - - * common case and for short inputs. - - */ + * If a value can overflow is original 32 bits by a factor of more than a few + * million (2^23 ish), there is a possibility that it might overflow the + * 53-bit mantissa and lose precision. + * + * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that + * propagates around the loop, and on the hash state h[]. I don't believe + * that the clamps on h4 and on h0 are strictly necessary, but it's close + * (for h4 anyway), and better safe than sorry. + * + * The clamps on h[] are necessary for the output to be correct even in the + * common case and for short inputs. + */ for (i = 0; i < 64; i++) { // load up the input word for this round @@ -784,19 +570,12 @@ sjcl.hash.sha256.prototype = { } h[0] = (h[0] + h0) | 0; - h[1] = (h[1] + h1) | 0; - h[2] = (h[2] + h2) | 0; - h[3] = (h[3] + h3) | 0; - h[4] = (h[4] + h4) | 0; - h[5] = (h[5] + h5) | 0; - h[6] = (h[6] + h6) | 0; - h[7] = (h[7] + h7) | 0; } }; From 0e676fe6248c09f886c9f2e522bf8d508be775ca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:18:18 -0500 Subject: [PATCH 19/60] remove some unused code --- packages/kit/src/runtime/server/page/sjcl.js | 143 ++----------------- 1 file changed, 15 insertions(+), 128 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index c45d73e24f1e..7fd1c5d2a23c 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -40,7 +40,7 @@ function toBits(str) { } if (i & 3) { - out.push(sjcl.bitArray.partial(8 * (i & 3), tmp)); + out.push(BitArray.partial(8 * (i & 3), tmp)); } return out; @@ -71,44 +71,9 @@ function toBits(str) { *

*/ -sjcl.bitArray = { - /** - * Array slices in units of bits. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} bend The offset to the end of the slice, in bits. If this is undefined, - * slice until the end of the array. - * @return {bitArray} The requested slice. - */ - bitSlice: function (a, bstart, bend) { - a = sjcl.bitArray._shiftRight(a.slice(bstart / 32), 32 - (bstart & 31)).slice(1); - return bend === undefined ? a : sjcl.bitArray.clamp(a, bend - bstart); - }, - - /** - * Extract a number packed into a bit array. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} blength The length of the number to extract. - * @return {Number} The requested slice. - */ - extract: function (a, bstart, blength) { - // FIXME: this Math.floor is not necessary at all, but for some reason - // seems to suppress a bug in the Chromium JIT. - var x, - sh = Math.floor((-bstart - blength) & 31); - - if (((bstart + blength - 1) ^ bstart) & -32) { - // it crosses a boundary - x = (a[(bstart / 32) | 0] << (32 - sh)) ^ (a[(bstart / 32 + 1) | 0] >>> sh); - } else { - // within a single word - x = a[(bstart / 32) | 0] >>> sh; - } - - return x & ((1 << blength) - 1); - }, +/** @typedef {number[]} bitArray */ +const BitArray = { /** * Concatenate two bit arrays. * @param {bitArray} a1 The first array. @@ -121,12 +86,12 @@ sjcl.bitArray = { } var last = a1[a1.length - 1], - shift = sjcl.bitArray.getPartial(last); + shift = BitArray.getPartial(last); if (shift === 32) { return a1.concat(a2); } else { - return sjcl.bitArray._shiftRight(a2, shift, last | 0, a1.slice(0, a1.length - 1)); + return BitArray._shiftRight(a2, shift, last | 0, a1.slice(0, a1.length - 1)); } }, @@ -145,31 +110,7 @@ sjcl.bitArray = { x = a[l - 1]; - return (l - 1) * 32 + sjcl.bitArray.getPartial(x); - }, - - /** - * Truncate an array. - * @param {bitArray} a The array. - * @param {Number} len The length to truncate to, in bits. - * @return {bitArray} A new array, truncated to len bits. - */ - clamp: function (a, len) { - if (a.length * 32 < len) { - return a; - } - - a = a.slice(0, Math.ceil(len / 32)); - - var l = a.length; - - len = len & 31; - - if (l > 0 && len) { - a[l - 1] = sjcl.bitArray.partial(len, a[l - 1] & (0x80000000 >> (len - 1)), 1); - } - - return a; + return (l - 1) * 32 + BitArray.getPartial(x); }, /** @@ -196,44 +137,18 @@ sjcl.bitArray = { return Math.round(x / 0x10000000000) || 32; }, - /** - * Compare two arrays for equality in a predictable amount of time. - * @param {bitArray} a The first array. - * @param {bitArray} b The second array. - * @return {boolean} true if a == b; false otherwise. - */ - - equal: function (a, b) { - if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) { - return false; - } - - var x = 0, - i; - - for (i = 0; i < a.length; i++) { - x |= a[i] ^ b[i]; - } - - return x === 0; - }, - /** Shift an array right. * @param {bitArray} a The array to shift. - * @param {Number} shift The number of bits to shift. - * @param {Number} [carry=0] A byte to carry in - * @param {bitArray} [out=[]] An array to prepend to the output. + * @param {number} shift The number of bits to shift. + * @param {number} [carry] A byte to carry in + * @param {bitArray} [out] An array to prepend to the output. * @private */ - _shiftRight: function (a, shift, carry, out) { + _shiftRight: function (a, shift, carry = 0, out = []) { var i, last2 = 0, shift2; - if (out === undefined) { - out = []; - } - for (; shift >= 32; shift -= 32) { out.push(carry); @@ -252,39 +167,11 @@ sjcl.bitArray = { last2 = a.length ? a[a.length - 1] : 0; - shift2 = sjcl.bitArray.getPartial(last2); + shift2 = BitArray.getPartial(last2); - out.push( - sjcl.bitArray.partial((shift + shift2) & 31, shift + shift2 > 32 ? carry : out.pop(), 1) - ); + out.push(BitArray.partial((shift + shift2) & 31, shift + shift2 > 32 ? carry : out.pop(), 1)); return out; - }, - - /** xor a block of 4 words together. - * @private - */ - _xor4: function (x, y) { - return [x[0] ^ y[0], x[1] ^ y[1], x[2] ^ y[2], x[3] ^ y[3]]; - }, - - /** byteswap a word array inplace. - * (does not handle partial words) - * @param {sjcl.bitArray} a word array - * @return {sjcl.bitArray} byteswapped array - */ - byteswapM: function (a) { - var i, - v, - m = 0xff00; - - for (i = 0; i < a.length; ++i) { - v = a[i]; - - a[i] = (v >>> 24) | ((v >>> 8) & m) | ((v & m) << 8) | (v << 24); - } - - return a; } }; @@ -362,9 +249,9 @@ sjcl.hash.sha256.prototype = { } var i, - b = (this._buffer = sjcl.bitArray.concat(this._buffer, data)), + b = (this._buffer = BitArray.concat(this._buffer, data)), ol = this._length, - nl = (this._length = ol + sjcl.bitArray.bitLength(data)); + nl = (this._length = ol + BitArray.bitLength(data)); if (nl > 9007199254740991) { throw new sjcl.exception.invalid('Cannot hash more than 2^53 - 1 bits'); @@ -402,7 +289,7 @@ sjcl.hash.sha256.prototype = { // Round out and push the buffer - b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1, 1)]); + b = BitArray.concat(b, [BitArray.partial(1, 1)]); // Round out the buffer to a multiple of 16 words, less the 2 length words. From cc9aca87a5332c6e5aabaf33028c2aa0d9b415cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:27:05 -0500 Subject: [PATCH 20/60] move some stuff out of the prototype --- packages/kit/src/runtime/server/page/sjcl.js | 72 +++++++++----------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 7fd1c5d2a23c..56e38036e340 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -175,6 +175,34 @@ const BitArray = { } }; +/** + * The SHA-256 initialization vector, to be precomputed. + * @private + */ +const init = []; + +/* + _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], + */ + +/** + * The SHA-256 hash key, to be precomputed. + * @private + */ +const key = []; + +/* + _key: + [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], + */ + /** @fileOverview Javascript SHA-256 implementation. * * An older version of this implementation is available in the public @@ -195,7 +223,7 @@ const BitArray = { */ sjcl.hash.sha256 = function (hash) { - if (!this._key[0]) { + if (!key[0]) { this._precompute(); } @@ -221,18 +249,12 @@ sjcl.hash.sha256.hash = function (data) { }; sjcl.hash.sha256.prototype = { - /** - * The hash's block size, in bits. - * @constant - */ - blockSize: 512, - /** * Reset the hash state. * @return this */ reset: function () { - this._h = this._init.slice(0); + this._h = init.slice(0); this._buffer = []; this._length = 0; return this; @@ -312,34 +334,6 @@ sjcl.hash.sha256.prototype = { return h; }, - /** - * The SHA-256 initialization vector, to be precomputed. - * @private - */ - _init: [], - - /* - _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], - */ - - /** - * The SHA-256 hash key, to be precomputed. - * @private - */ - _key: [], - - /* - _key: - [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], - */ - /** * Function to precompute _init and _key. * @private @@ -367,10 +361,10 @@ sjcl.hash.sha256.prototype = { if (isPrime) { if (i < 8) { - this._init[i] = frac(Math.pow(prime, 1 / 2)); + init[i] = frac(Math.pow(prime, 1 / 2)); } - this._key[i] = frac(Math.pow(prime, 1 / 3)); + key[i] = frac(Math.pow(prime, 1 / 3)); i++; } @@ -388,7 +382,7 @@ sjcl.hash.sha256.prototype = { a, b, h = this._h, - k = this._key, + k = key, h0 = h[0], h1 = h[1], h2 = h[2], From 07f86fc59eaa53369dbefc37088449e7345d7a26 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:29:27 -0500 Subject: [PATCH 21/60] use a class --- .../kit/src/runtime/server/page/crypto.js | 4 +- packages/kit/src/runtime/server/page/sjcl.js | 62 +++++++++---------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/kit/src/runtime/server/page/crypto.js b/packages/kit/src/runtime/server/page/crypto.js index 450cc310a808..f2e5a57ac55f 100644 --- a/packages/kit/src/runtime/server/page/crypto.js +++ b/packages/kit/src/runtime/server/page/crypto.js @@ -1,11 +1,11 @@ -import { sjcl } from './sjcl.js'; +import { sjcl, Sha256 } from './sjcl.js'; // adapted from https://bitwiseshiftleft.github.io/sjcl/, // modified and redistributed under BSD license /** @param {string} text */ export function sha256(text) { - const hashed = new Uint32Array(sjcl.hash.sha256.hash(text)); + const hashed = new Uint32Array(Sha256.hash(text)); const buffer = hashed.buffer; const uint8array = new Uint8Array(buffer); diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 56e38036e340..7b0a28dec086 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -222,50 +222,50 @@ const key = []; * @constructor */ -sjcl.hash.sha256 = function (hash) { - if (!key[0]) { - this._precompute(); - } +export class Sha256 { + constructor(hash) { + if (!key[0]) { + this._precompute(); + } - if (hash) { - this._h = hash._h.slice(0); + if (hash) { + this._h = hash._h.slice(0); - this._buffer = hash._buffer.slice(0); + this._buffer = hash._buffer.slice(0); - this._length = hash._length; - } else { - this.reset(); + this._length = hash._length; + } else { + this.reset(); + } } -}; -/** - * Hash a string or an array of words. - * @static - * @param {bitArray|String} data the data to hash. - * @return {bitArray} The hash value, an array of 16 big-endian words. - */ -sjcl.hash.sha256.hash = function (data) { - return new sjcl.hash.sha256().update(data).finalize(); -}; + /** + * Hash a string or an array of words. + * @static + * @param {bitArray|String} data the data to hash. + * @return {bitArray} The hash value, an array of 16 big-endian words. + */ + static hash(data) { + return new Sha256().update(data).finalize(); + } -sjcl.hash.sha256.prototype = { /** * Reset the hash state. * @return this */ - reset: function () { + reset() { this._h = init.slice(0); this._buffer = []; this._length = 0; return this; - }, + } /** * Input several words to the hash. * @param {bitArray|String} data the data to hash. * @return this */ - update: function (data) { + update(data) { if (typeof data === 'string') { data = toBits(data); } @@ -298,13 +298,13 @@ sjcl.hash.sha256.prototype = { } return this; - }, + } /** * Complete hashing and output the hash value. * @return {bitArray} The hash value, an array of 8 big-endian words. */ - finalize: function () { + finalize() { var i, b = this._buffer, h = this._h; @@ -332,13 +332,13 @@ sjcl.hash.sha256.prototype = { this.reset(); return h; - }, + } /** * Function to precompute _init and _key. * @private */ - _precompute: function () { + _precompute() { var i = 0, prime = 2, factor, @@ -369,14 +369,14 @@ sjcl.hash.sha256.prototype = { i++; } } - }, + } /** * Perform one cycle of SHA-256. * @param {Uint32Array|bitArray} w one block of words. * @private */ - _block: function (w) { + _block(w) { var i, tmp, a, @@ -459,4 +459,4 @@ sjcl.hash.sha256.prototype = { h[6] = (h[6] + h6) | 0; h[7] = (h[7] + h7) | 0; } -}; +} From 3ec91c6558fb0b7f8b109b48a0e025045c08577e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:31:24 -0500 Subject: [PATCH 22/60] more tidying --- packages/kit/src/runtime/server/page/sjcl.js | 70 +++++--------------- 1 file changed, 16 insertions(+), 54 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 7b0a28dec086..62b9fa10512e 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -177,66 +177,41 @@ const BitArray = { /** * The SHA-256 initialization vector, to be precomputed. - * @private + * @type {number[]} */ const init = []; /* - _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], + * init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], */ /** * The SHA-256 hash key, to be precomputed. - * @private + * @type {number[]} */ const key = []; /* - _key: - [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], - */ - -/** @fileOverview Javascript SHA-256 implementation. - * - * An older version of this implementation is available in the public - * domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh, - * Stanford University 2008-2010 and BSD-licensed for liability - * reasons. - * - * Special thanks to Aldo Cortesi for pointing out several bugs in - * this code. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ -/** - * Context for a SHA-256 operation in progress. - * @constructor + * key: + * [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + * 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + * 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + * 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + * 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + * 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + * 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + * 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], */ export class Sha256 { - constructor(hash) { + constructor() { if (!key[0]) { this._precompute(); } - if (hash) { - this._h = hash._h.slice(0); - - this._buffer = hash._buffer.slice(0); - - this._length = hash._length; - } else { - this.reset(); - } + this._h = init.slice(0); + this._buffer = []; + this._length = 0; } /** @@ -249,17 +224,6 @@ export class Sha256 { return new Sha256().update(data).finalize(); } - /** - * Reset the hash state. - * @return this - */ - reset() { - this._h = init.slice(0); - this._buffer = []; - this._length = 0; - return this; - } - /** * Input several words to the hash. * @param {bitArray|String} data the data to hash. @@ -329,8 +293,6 @@ export class Sha256 { this._block(b.splice(0, 16)); } - this.reset(); - return h; } From 527bb33fc748b52df08e3fd2317ebffe70778c8d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:33:27 -0500 Subject: [PATCH 23/60] more tidying --- packages/kit/src/runtime/server/page/sjcl.js | 87 ++++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 62b9fa10512e..a95f46c767d5 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -203,10 +203,47 @@ const key = []; * 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], */ +/** + * Function to precompute _init and _key. + */ +function precompute() { + var i = 0, + prime = 2, + factor, + isPrime; + + /** @param {number} x */ + function frac(x) { + return ((x - Math.floor(x)) * 0x100000000) | 0; + } + + for (; i < 64; prime++) { + isPrime = true; + + for (factor = 2; factor * factor <= prime; factor++) { + if (prime % factor === 0) { + isPrime = false; + + break; + } + } + + if (isPrime) { + if (i < 8) { + init[i] = frac(Math.pow(prime, 1 / 2)); + } + + key[i] = frac(Math.pow(prime, 1 / 3)); + + i++; + } + } +} + export class Sha256 { constructor() { if (!key[0]) { - this._precompute(); + precompute(); } this._h = init.slice(0); @@ -240,7 +277,7 @@ export class Sha256 { nl = (this._length = ol + BitArray.bitLength(data)); if (nl > 9007199254740991) { - throw new sjcl.exception.invalid('Cannot hash more than 2^53 - 1 bits'); + throw new Error('Cannot hash more than 2^53 - 1 bits'); } if (typeof Uint32Array !== 'undefined') { @@ -249,7 +286,7 @@ export class Sha256 { var j = 0; for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { - this._block(c.subarray(16 * j, 16 * (j + 1))); + this.#block(c.subarray(16 * j, 16 * (j + 1))); j += 1; } @@ -257,7 +294,7 @@ export class Sha256 { b.splice(0, 16 * j); } else { for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { - this._block(b.splice(0, 16)); + this.#block(b.splice(0, 16)); } } @@ -290,55 +327,17 @@ export class Sha256 { b.push(this._length | 0); while (b.length) { - this._block(b.splice(0, 16)); + this.#block(b.splice(0, 16)); } return h; } - /** - * Function to precompute _init and _key. - * @private - */ - _precompute() { - var i = 0, - prime = 2, - factor, - isPrime; - - function frac(x) { - return ((x - Math.floor(x)) * 0x100000000) | 0; - } - - for (; i < 64; prime++) { - isPrime = true; - - for (factor = 2; factor * factor <= prime; factor++) { - if (prime % factor === 0) { - isPrime = false; - - break; - } - } - - if (isPrime) { - if (i < 8) { - init[i] = frac(Math.pow(prime, 1 / 2)); - } - - key[i] = frac(Math.pow(prime, 1 / 3)); - - i++; - } - } - } - /** * Perform one cycle of SHA-256. * @param {Uint32Array|bitArray} w one block of words. - * @private */ - _block(w) { + #block(w) { var i, tmp, a, From e5cf81dc05d7b5305c504d522e9d0052f02e19fd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:38:21 -0500 Subject: [PATCH 24/60] more tidying --- .../kit/src/runtime/server/page/crypto.js | 2 +- packages/kit/src/runtime/server/page/sjcl.js | 29 ++++++------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/kit/src/runtime/server/page/crypto.js b/packages/kit/src/runtime/server/page/crypto.js index f2e5a57ac55f..57bf63ca2479 100644 --- a/packages/kit/src/runtime/server/page/crypto.js +++ b/packages/kit/src/runtime/server/page/crypto.js @@ -1,4 +1,4 @@ -import { sjcl, Sha256 } from './sjcl.js'; +import { Sha256 } from './sjcl.js'; // adapted from https://bitwiseshiftleft.github.io/sjcl/, // modified and redistributed under BSD license diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index a95f46c767d5..0e3ba3744b4f 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -1,7 +1,3 @@ -export const sjcl = { - hash: {} -}; - /** @param {Uint32Array} uint32array' */ function swap_endianness(uint32array) { const uint8array = new Uint8Array(uint32array.buffer); @@ -254,7 +250,7 @@ export class Sha256 { /** * Hash a string or an array of words. * @static - * @param {bitArray|String} data the data to hash. + * @param {bitArray | string} data the data to hash. * @return {bitArray} The hash value, an array of 16 big-endian words. */ static hash(data) { @@ -263,8 +259,7 @@ export class Sha256 { /** * Input several words to the hash. - * @param {bitArray|String} data the data to hash. - * @return this + * @param {bitArray | string} data the data to hash. */ update(data) { if (typeof data === 'string') { @@ -280,24 +275,18 @@ export class Sha256 { throw new Error('Cannot hash more than 2^53 - 1 bits'); } - if (typeof Uint32Array !== 'undefined') { - var c = new Uint32Array(b); + var c = new Uint32Array(b); - var j = 0; + var j = 0; - for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { - this.#block(c.subarray(16 * j, 16 * (j + 1))); - - j += 1; - } + for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { + this.#block(c.subarray(16 * j, 16 * (j + 1))); - b.splice(0, 16 * j); - } else { - for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { - this.#block(b.splice(0, 16)); - } + j += 1; } + b.splice(0, 16 * j); + return this; } From 1f2015a9ce04f94873ccabef542a63c0090e8ba4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:40:49 -0500 Subject: [PATCH 25/60] fix all type errors --- packages/kit/src/runtime/server/page/sjcl.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 0e3ba3744b4f..677282c94d56 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -94,7 +94,7 @@ const BitArray = { /** * Find the length of an array of bits. * @param {bitArray} a The array. - * @return {Number} The length of a, in bits. + * @return {number} The length of a, in bits. */ bitLength: function (a) { var l = a.length, @@ -165,7 +165,13 @@ const BitArray = { shift2 = BitArray.getPartial(last2); - out.push(BitArray.partial((shift + shift2) & 31, shift + shift2 > 32 ? carry : out.pop(), 1)); + out.push( + BitArray.partial( + (shift + shift2) & 31, + shift + shift2 > 32 ? carry : /** @type {number} */ (out.pop()), + 1 + ) + ); return out; } @@ -243,6 +249,7 @@ export class Sha256 { } this._h = init.slice(0); + /** @type {bitArray} */ this._buffer = []; this._length = 0; } From d226b5b2b296f4b8d4483c45d299cc526fe0ff4a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:44:47 -0500 Subject: [PATCH 26/60] store init vector and hash key as typed arrays --- packages/kit/src/runtime/server/page/sjcl.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 677282c94d56..459f242d41df 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -179,9 +179,8 @@ const BitArray = { /** * The SHA-256 initialization vector, to be precomputed. - * @type {number[]} */ -const init = []; +const init = new Uint32Array(8); /* * init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], @@ -189,9 +188,8 @@ const init = []; /** * The SHA-256 hash key, to be precomputed. - * @type {number[]} */ -const key = []; +const key = new Uint32Array(64); /* * key: @@ -216,7 +214,7 @@ function precompute() { /** @param {number} x */ function frac(x) { - return ((x - Math.floor(x)) * 0x100000000) | 0; + return (x - Math.floor(x)) * 0x100000000; } for (; i < 64; prime++) { From 7dc289b2fc8c8c0e1ed0d99dab25c60557c1f3f5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 10:53:50 -0500 Subject: [PATCH 27/60] convert to closure --- .../kit/src/runtime/server/page/crypto.js | 5 +- packages/kit/src/runtime/server/page/sjcl.js | 137 ++++++++---------- 2 files changed, 60 insertions(+), 82 deletions(-) diff --git a/packages/kit/src/runtime/server/page/crypto.js b/packages/kit/src/runtime/server/page/crypto.js index 57bf63ca2479..ebceeb5e2902 100644 --- a/packages/kit/src/runtime/server/page/crypto.js +++ b/packages/kit/src/runtime/server/page/crypto.js @@ -1,11 +1,12 @@ -import { Sha256 } from './sjcl.js'; +import { hash } from './sjcl.js'; // adapted from https://bitwiseshiftleft.github.io/sjcl/, // modified and redistributed under BSD license /** @param {string} text */ export function sha256(text) { - const hashed = new Uint32Array(Sha256.hash(text)); + // const hashed = new Uint32Array(Sha256.hash(text)); + const hashed = new Uint32Array(hash(text)); const buffer = hashed.buffer; const uint8array = new Uint8Array(buffer); diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 459f242d41df..d4841ea1eb74 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -240,103 +240,45 @@ function precompute() { } } -export class Sha256 { - constructor() { - if (!key[0]) { - precompute(); - } - - this._h = init.slice(0); - /** @type {bitArray} */ - this._buffer = []; - this._length = 0; - } - - /** - * Hash a string or an array of words. - * @static - * @param {bitArray | string} data the data to hash. - * @return {bitArray} The hash value, an array of 16 big-endian words. - */ - static hash(data) { - return new Sha256().update(data).finalize(); +/** @param {bitArray | string} data */ +export function hash(data) { + if (!key[0]) { + precompute(); } - /** - * Input several words to the hash. - * @param {bitArray | string} data the data to hash. - */ - update(data) { - if (typeof data === 'string') { - data = toBits(data); - } - - var i, - b = (this._buffer = BitArray.concat(this._buffer, data)), - ol = this._length, - nl = (this._length = ol + BitArray.bitLength(data)); - - if (nl > 9007199254740991) { - throw new Error('Cannot hash more than 2^53 - 1 bits'); - } - - var c = new Uint32Array(b); - - var j = 0; - - for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { - this.#block(c.subarray(16 * j, 16 * (j + 1))); - - j += 1; - } + const _h = init.slice(0); + /** @type {bitArray} */ + let _buffer = []; + let _length = 0; - b.splice(0, 16 * j); - - return this; + if (typeof data === 'string') { + data = toBits(data); } - /** - * Complete hashing and output the hash value. - * @return {bitArray} The hash value, an array of 8 big-endian words. - */ - finalize() { - var i, - b = this._buffer, - h = this._h; - - // Round out and push the buffer - - b = BitArray.concat(b, [BitArray.partial(1, 1)]); + // update + var i, + b = (_buffer = BitArray.concat(_buffer, data)), + ol = _length, + nl = (_length = ol + BitArray.bitLength(data)); - // Round out the buffer to a multiple of 16 words, less the 2 length words. - - for (i = b.length + 2; i & 15; i++) { - b.push(0); - } - - // append the length - - b.push(Math.floor(this._length / 0x100000000)); - - b.push(this._length | 0); + if (nl > 9007199254740991) { + throw new Error('Cannot hash more than 2^53 - 1 bits'); + } - while (b.length) { - this.#block(b.splice(0, 16)); - } + var c = new Uint32Array(b); - return h; - } + var j = 0; /** * Perform one cycle of SHA-256. * @param {Uint32Array|bitArray} w one block of words. */ - #block(w) { + const block = (w) => { var i, tmp, a, b, - h = this._h, + h = _h, k = key, h0 = h[0], h1 = h[1], @@ -413,5 +355,40 @@ export class Sha256 { h[5] = (h[5] + h5) | 0; h[6] = (h[6] + h6) | 0; h[7] = (h[7] + h7) | 0; + }; + + for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { + block(c.subarray(16 * j, 16 * (j + 1))); + + j += 1; } + + b.splice(0, 16 * j); + + // finalize + var i, + b = _buffer, + h = _h; + + // Round out and push the buffer + + b = BitArray.concat(b, [BitArray.partial(1, 1)]); + + // Round out the buffer to a multiple of 16 words, less the 2 length words. + + for (i = b.length + 2; i & 15; i++) { + b.push(0); + } + + // append the length + + b.push(Math.floor(_length / 0x100000000)); + + b.push(_length | 0); + + while (b.length) { + block(b.splice(0, 16)); + } + + return h; } From b2f77bc28c230c77499760911b8726088c9b40aa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 11:11:01 -0500 Subject: [PATCH 28/60] more tidying --- packages/kit/src/runtime/server/page/sjcl.js | 140 ++++++++----------- 1 file changed, 62 insertions(+), 78 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index d4841ea1eb74..328687fb3ccb 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -242,52 +242,38 @@ function precompute() { /** @param {bitArray | string} data */ export function hash(data) { - if (!key[0]) { - precompute(); - } - - const _h = init.slice(0); - /** @type {bitArray} */ - let _buffer = []; - let _length = 0; + if (!key[0]) precompute(); if (typeof data === 'string') { data = toBits(data); } - // update - var i, - b = (_buffer = BitArray.concat(_buffer, data)), - ol = _length, - nl = (_length = ol + BitArray.bitLength(data)); - - if (nl > 9007199254740991) { - throw new Error('Cannot hash more than 2^53 - 1 bits'); - } + const out = init.slice(0); - var c = new Uint32Array(b); + /** @type {bitArray} */ + let _buffer = data; + let _length = BitArray.bitLength(data); - var j = 0; + // update + const c = new Uint32Array(_buffer); /** * Perform one cycle of SHA-256. * @param {Uint32Array|bitArray} w one block of words. */ const block = (w) => { - var i, - tmp, - a, - b, - h = _h, - k = key, - h0 = h[0], - h1 = h[1], - h2 = h[2], - h3 = h[3], - h4 = h[4], - h5 = h[5], - h6 = h[6], - h7 = h[7]; + var tmp; + var a; + var b; + + let out0 = out[0]; + let out1 = out[1]; + let out2 = out[2]; + let out3 = out[3]; + let out4 = out[4]; + let out5 = out[5]; + let out6 = out[6]; + let out7 = out[7]; /* Rationale for placement of |0 : * If a value can overflow is original 32 bits by a factor of more than a few @@ -295,15 +281,15 @@ export function hash(data) { * 53-bit mantissa and lose precision. * * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that - * propagates around the loop, and on the hash state h[]. I don't believe - * that the clamps on h4 and on h0 are strictly necessary, but it's close - * (for h4 anyway), and better safe than sorry. + * propagates around the loop, and on the hash state out[]. I don't believe + * that the clamps on out4 and on out0 are strictly necessary, but it's close + * (for out4 anyway), and better safe than sorry. * - * The clamps on h[] are necessary for the output to be correct even in the + * The clamps on out[] are necessary for the output to be correct even in the * common case and for short inputs. */ - for (i = 0; i < 64; i++) { + for (let i = 0; i < 64; i++) { // load up the input word for this round if (i < 16) { @@ -323,72 +309,70 @@ export function hash(data) { tmp = tmp + - h7 + - ((h4 >>> 6) ^ (h4 >>> 11) ^ (h4 >>> 25) ^ (h4 << 26) ^ (h4 << 21) ^ (h4 << 7)) + - (h6 ^ (h4 & (h5 ^ h6))) + - k[i]; // | 0; + out7 + + ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + + (out6 ^ (out4 & (out5 ^ out6))) + + key[i]; // | 0; // shift register + out7 = out6; + out6 = out5; + out5 = out4; - h7 = h6; - h6 = h5; - h5 = h4; - - h4 = (h3 + tmp) | 0; + out4 = (out3 + tmp) | 0; - h3 = h2; - h2 = h1; - h1 = h0; + out3 = out2; + out2 = out1; + out1 = out0; - h0 = + out0 = (tmp + - ((h1 & h2) ^ (h3 & (h1 ^ h2))) + - ((h1 >>> 2) ^ (h1 >>> 13) ^ (h1 >>> 22) ^ (h1 << 30) ^ (h1 << 19) ^ (h1 << 10))) | + ((out1 & out2) ^ (out3 & (out1 ^ out2))) + + ((out1 >>> 2) ^ + (out1 >>> 13) ^ + (out1 >>> 22) ^ + (out1 << 30) ^ + (out1 << 19) ^ + (out1 << 10))) | 0; } - h[0] = (h[0] + h0) | 0; - h[1] = (h[1] + h1) | 0; - h[2] = (h[2] + h2) | 0; - h[3] = (h[3] + h3) | 0; - h[4] = (h[4] + h4) | 0; - h[5] = (h[5] + h5) | 0; - h[6] = (h[6] + h6) | 0; - h[7] = (h[7] + h7) | 0; + out[0] = (out[0] + out0) | 0; + out[1] = (out[1] + out1) | 0; + out[2] = (out[2] + out2) | 0; + out[3] = (out[3] + out3) | 0; + out[4] = (out[4] + out4) | 0; + out[5] = (out[5] + out5) | 0; + out[6] = (out[6] + out6) | 0; + out[7] = (out[7] + out7) | 0; }; - for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { + let j = 0; + for (let i = 512; i <= _length; i += 512) { block(c.subarray(16 * j, 16 * (j + 1))); j += 1; } - b.splice(0, 16 * j); + _buffer.splice(0, 16 * j); // finalize - var i, - b = _buffer, - h = _h; // Round out and push the buffer - - b = BitArray.concat(b, [BitArray.partial(1, 1)]); + _buffer = BitArray.concat(_buffer, [BitArray.partial(1, 1)]); // Round out the buffer to a multiple of 16 words, less the 2 length words. - - for (i = b.length + 2; i & 15; i++) { - b.push(0); + for (let i = _buffer.length + 2; i & 15; i++) { + _buffer.push(0); } // append the length + _buffer.push(Math.floor(_length / 0x100000000)); + _buffer.push(_length | 0); - b.push(Math.floor(_length / 0x100000000)); - - b.push(_length | 0); - - while (b.length) { - block(b.splice(0, 16)); + while (_buffer.length) { + block(_buffer.splice(0, 16)); } - return h; + return out; } From 58a22a1a1122eb2a89b5f87877dacdc84aec308b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 11:14:20 -0500 Subject: [PATCH 29/60] hoist block --- packages/kit/src/runtime/server/page/sjcl.js | 186 +++++++++---------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 328687fb3ccb..5eb690a0de63 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -240,6 +240,97 @@ function precompute() { } } +/** + * Perform one cycle of SHA-256. + * @param {Uint32Array} out + * @param {Uint32Array | bitArray} w one block of words. + */ +const block = (out, w) => { + var tmp; + var a; + var b; + + let out0 = out[0]; + let out1 = out[1]; + let out2 = out[2]; + let out3 = out[3]; + let out4 = out[4]; + let out5 = out[5]; + let out6 = out[6]; + let out7 = out[7]; + + /* Rationale for placement of |0 : + * If a value can overflow is original 32 bits by a factor of more than a few + * million (2^23 ish), there is a possibility that it might overflow the + * 53-bit mantissa and lose precision. + * + * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that + * propagates around the loop, and on the hash state out[]. I don't believe + * that the clamps on out4 and on out0 are strictly necessary, but it's close + * (for out4 anyway), and better safe than sorry. + * + * The clamps on out[] are necessary for the output to be correct even in the + * common case and for short inputs. + */ + + for (let i = 0; i < 64; i++) { + // load up the input word for this round + + if (i < 16) { + tmp = w[i]; + } else { + a = w[(i + 1) & 15]; + + b = w[(i + 14) & 15]; + + tmp = w[i & 15] = + (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + + ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + + w[i & 15] + + w[(i + 9) & 15]) | + 0; + } + + tmp = + tmp + + out7 + + ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + + (out6 ^ (out4 & (out5 ^ out6))) + + key[i]; // | 0; + + // shift register + out7 = out6; + out6 = out5; + out5 = out4; + + out4 = (out3 + tmp) | 0; + + out3 = out2; + out2 = out1; + out1 = out0; + + out0 = + (tmp + + ((out1 & out2) ^ (out3 & (out1 ^ out2))) + + ((out1 >>> 2) ^ + (out1 >>> 13) ^ + (out1 >>> 22) ^ + (out1 << 30) ^ + (out1 << 19) ^ + (out1 << 10))) | + 0; + } + + out[0] = (out[0] + out0) | 0; + out[1] = (out[1] + out1) | 0; + out[2] = (out[2] + out2) | 0; + out[3] = (out[3] + out3) | 0; + out[4] = (out[4] + out4) | 0; + out[5] = (out[5] + out5) | 0; + out[6] = (out[6] + out6) | 0; + out[7] = (out[7] + out7) | 0; +}; + /** @param {bitArray | string} data */ export function hash(data) { if (!key[0]) precompute(); @@ -257,100 +348,9 @@ export function hash(data) { // update const c = new Uint32Array(_buffer); - /** - * Perform one cycle of SHA-256. - * @param {Uint32Array|bitArray} w one block of words. - */ - const block = (w) => { - var tmp; - var a; - var b; - - let out0 = out[0]; - let out1 = out[1]; - let out2 = out[2]; - let out3 = out[3]; - let out4 = out[4]; - let out5 = out[5]; - let out6 = out[6]; - let out7 = out[7]; - - /* Rationale for placement of |0 : - * If a value can overflow is original 32 bits by a factor of more than a few - * million (2^23 ish), there is a possibility that it might overflow the - * 53-bit mantissa and lose precision. - * - * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that - * propagates around the loop, and on the hash state out[]. I don't believe - * that the clamps on out4 and on out0 are strictly necessary, but it's close - * (for out4 anyway), and better safe than sorry. - * - * The clamps on out[] are necessary for the output to be correct even in the - * common case and for short inputs. - */ - - for (let i = 0; i < 64; i++) { - // load up the input word for this round - - if (i < 16) { - tmp = w[i]; - } else { - a = w[(i + 1) & 15]; - - b = w[(i + 14) & 15]; - - tmp = w[i & 15] = - (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + - ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + - w[i & 15] + - w[(i + 9) & 15]) | - 0; - } - - tmp = - tmp + - out7 + - ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + - (out6 ^ (out4 & (out5 ^ out6))) + - key[i]; // | 0; - - // shift register - out7 = out6; - out6 = out5; - out5 = out4; - - out4 = (out3 + tmp) | 0; - - out3 = out2; - out2 = out1; - out1 = out0; - - out0 = - (tmp + - ((out1 & out2) ^ (out3 & (out1 ^ out2))) + - ((out1 >>> 2) ^ - (out1 >>> 13) ^ - (out1 >>> 22) ^ - (out1 << 30) ^ - (out1 << 19) ^ - (out1 << 10))) | - 0; - } - - out[0] = (out[0] + out0) | 0; - out[1] = (out[1] + out1) | 0; - out[2] = (out[2] + out2) | 0; - out[3] = (out[3] + out3) | 0; - out[4] = (out[4] + out4) | 0; - out[5] = (out[5] + out5) | 0; - out[6] = (out[6] + out6) | 0; - out[7] = (out[7] + out7) | 0; - }; - let j = 0; for (let i = 512; i <= _length; i += 512) { - block(c.subarray(16 * j, 16 * (j + 1))); - + block(out, c.subarray(16 * j, 16 * (j + 1))); j += 1; } @@ -371,7 +371,7 @@ export function hash(data) { _buffer.push(_length | 0); while (_buffer.length) { - block(_buffer.splice(0, 16)); + block(out, _buffer.splice(0, 16)); } return out; From 277658e2252678e6b834af68fbd53818f97a06ad Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 11:33:53 -0500 Subject: [PATCH 30/60] more tidying --- packages/kit/src/runtime/server/page/sjcl.js | 37 +++++++++----------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 5eb690a0de63..8e3b41ef0de1 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -21,9 +21,10 @@ function swap_endianness(uint32array) { function toBits(str) { str = unescape(encodeURIComponent(str)); - var out = [], - i, - tmp = 0; + const out = []; + + let i; + let tmp = 0; for (i = 0; i < str.length; i++) { tmp = (tmp << 8) | str.charCodeAt(i); @@ -331,47 +332,43 @@ const block = (out, w) => { out[7] = (out[7] + out7) | 0; }; -/** @param {bitArray | string} data */ +/** @param {string} data */ export function hash(data) { if (!key[0]) precompute(); - if (typeof data === 'string') { - data = toBits(data); - } - const out = init.slice(0); /** @type {bitArray} */ - let _buffer = data; - let _length = BitArray.bitLength(data); + let buffer = toBits(data); + let length = BitArray.bitLength(buffer); // update - const c = new Uint32Array(_buffer); + const c = new Uint32Array(buffer); let j = 0; - for (let i = 512; i <= _length; i += 512) { + for (let i = 512; i <= length; i += 512) { block(out, c.subarray(16 * j, 16 * (j + 1))); j += 1; } - _buffer.splice(0, 16 * j); + buffer.splice(0, 16 * j); // finalize // Round out and push the buffer - _buffer = BitArray.concat(_buffer, [BitArray.partial(1, 1)]); + buffer = BitArray.concat(buffer, [BitArray.partial(1, 1)]); // Round out the buffer to a multiple of 16 words, less the 2 length words. - for (let i = _buffer.length + 2; i & 15; i++) { - _buffer.push(0); + for (let i = buffer.length + 2; i & 15; i++) { + buffer.push(0); } // append the length - _buffer.push(Math.floor(_length / 0x100000000)); - _buffer.push(_length | 0); + buffer.push(Math.floor(length / 0x100000000)); + buffer.push(length | 0); - while (_buffer.length) { - block(out, _buffer.splice(0, 16)); + while (buffer.length) { + block(out, buffer.splice(0, 16)); } return out; From 4517b3bedc9fd14a16e6fb1f198e91909b5f5599 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 12:18:51 -0500 Subject: [PATCH 31/60] use textdecoder --- packages/kit/src/runtime/server/page/crypto.spec.js | 6 +++++- packages/kit/src/runtime/server/page/sjcl.js | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/runtime/server/page/crypto.spec.js b/packages/kit/src/runtime/server/page/crypto.spec.js index d620f39b514a..dda6e0985302 100644 --- a/packages/kit/src/runtime/server/page/crypto.spec.js +++ b/packages/kit/src/runtime/server/page/crypto.spec.js @@ -3,7 +3,11 @@ import * as assert from 'uvu/assert'; import crypto from 'crypto'; import { sha256 } from './crypto.js'; -const inputs = ['the quick brown fox jumps over the lazy dog', '工欲善其事,必先利其器']; +const inputs = [ + 'abc', + 'the quick brown fox jumps over the lazy dog', + '工欲善其事,必先利其器' +].slice(0); inputs.forEach((input) => { test(input, () => { diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 8e3b41ef0de1..4e2af2477139 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -19,25 +19,25 @@ function swap_endianness(uint32array) { /** @param {string} str */ function toBits(str) { - str = unescape(encodeURIComponent(str)); + let uint8array = new TextEncoder().encode(str); const out = []; let i; let tmp = 0; - for (i = 0; i < str.length; i++) { - tmp = (tmp << 8) | str.charCodeAt(i); + for (i = 0; i < uint8array.length; i++) { + tmp = (tmp << 8) | uint8array[i]; if ((i & 3) === 3) { out.push(tmp); - tmp = 0; } } if (i & 3) { - out.push(BitArray.partial(8 * (i & 3), tmp)); + let partial = BitArray.partial(8 * (i & 3), tmp); + out.push(partial); } return out; From 90de3b5f731f1f1c151243261bd27ec1f45253d1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 12:19:12 -0500 Subject: [PATCH 32/60] create textencoder once --- packages/kit/src/runtime/server/page/sjcl.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 4e2af2477139..eff5a0e51a16 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -17,9 +17,11 @@ function swap_endianness(uint32array) { return uint32array; } +const encoder = new TextEncoder(); + /** @param {string} str */ function toBits(str) { - let uint8array = new TextEncoder().encode(str); + let uint8array = encoder.encode(str); const out = []; From 532471c65a40247c0ba0b73d4c6704a80818c6d5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 12:21:02 -0500 Subject: [PATCH 33/60] more tidying --- packages/kit/src/runtime/server/page/sjcl.js | 21 +++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index eff5a0e51a16..bd4a8204f344 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -210,33 +210,30 @@ const key = new Uint32Array(64); * Function to precompute _init and _key. */ function precompute() { - var i = 0, - prime = 2, - factor, - isPrime; - /** @param {number} x */ function frac(x) { return (x - Math.floor(x)) * 0x100000000; } - for (; i < 64; prime++) { - isPrime = true; + let prime = 2; + + for (let i = 0; i < 64; prime++) { + let is_prime = true; - for (factor = 2; factor * factor <= prime; factor++) { + for (let factor = 2; factor * factor <= prime; factor++) { if (prime % factor === 0) { - isPrime = false; + is_prime = false; break; } } - if (isPrime) { + if (is_prime) { if (i < 8) { - init[i] = frac(Math.pow(prime, 1 / 2)); + init[i] = frac(prime ** (1 / 2)); } - key[i] = frac(Math.pow(prime, 1 / 3)); + key[i] = frac(prime ** (1 / 3)); i++; } From 9b87c5444c30dff3ab017ed05d983b6851c204ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 13:52:39 -0500 Subject: [PATCH 34/60] simplify further --- .../src/runtime/server/page/crypto.spec.js | 3 ++- packages/kit/src/runtime/server/page/sjcl.js | 21 +++++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/kit/src/runtime/server/page/crypto.spec.js b/packages/kit/src/runtime/server/page/crypto.spec.js index dda6e0985302..a46a84018462 100644 --- a/packages/kit/src/runtime/server/page/crypto.spec.js +++ b/packages/kit/src/runtime/server/page/crypto.spec.js @@ -14,7 +14,8 @@ inputs.forEach((input) => { const expected_bytes = crypto.createHash('sha256').update(input, 'utf-8').digest(); const expected = expected_bytes.toString('base64'); - assert.equal(sha256(input), expected); + const actual = sha256(input); + assert.equal(actual, expected); }); }); diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index bd4a8204f344..7aba1cae7b8f 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -243,7 +243,7 @@ function precompute() { /** * Perform one cycle of SHA-256. * @param {Uint32Array} out - * @param {Uint32Array | bitArray} w one block of words. + * @param {Uint32Array} w one block of words. */ const block = (out, w) => { var tmp; @@ -341,19 +341,6 @@ export function hash(data) { let buffer = toBits(data); let length = BitArray.bitLength(buffer); - // update - const c = new Uint32Array(buffer); - - let j = 0; - for (let i = 512; i <= length; i += 512) { - block(out, c.subarray(16 * j, 16 * (j + 1))); - j += 1; - } - - buffer.splice(0, 16 * j); - - // finalize - // Round out and push the buffer buffer = BitArray.concat(buffer, [BitArray.partial(1, 1)]); @@ -366,8 +353,10 @@ export function hash(data) { buffer.push(Math.floor(length / 0x100000000)); buffer.push(length | 0); - while (buffer.length) { - block(out, buffer.splice(0, 16)); + const uint32array = new Uint32Array(buffer); + + for (let i = 0; i < uint32array.length; i += 16) { + block(out, uint32array.subarray(i, i + 16)); } return out; From dfb2c5d7979a79fbf845b3c11bcae29efb2f15d4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 14:45:33 -0500 Subject: [PATCH 35/60] more tidying --- packages/kit/src/runtime/server/page/sjcl.js | 106 +++++++++---------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 7aba1cae7b8f..ee9d3838b073 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -1,3 +1,5 @@ +const encoder = new TextEncoder(); + /** @param {Uint32Array} uint32array' */ function swap_endianness(uint32array) { const uint8array = new Uint8Array(uint32array.buffer); @@ -17,7 +19,27 @@ function swap_endianness(uint32array) { return uint32array; } -const encoder = new TextEncoder(); +/** + * Get the number of bits used by a partial word. + * @param {number} n The partial word. + * @return {number} The number of bits used by the partial word. + */ +function count_bits_in_word(n) { + return Math.round(n / 0x10000000000) || 32; +} + +/** + * Find the length of an array of bits. + * @param {Uint32Array} a The array. + * @return {number} The length of a, in bits. + */ +function count_bits_in_buffer(a) { + const l = a.length; + if (l === 0) return 0; + + const x = a[l - 1]; + return (l - 1) * 32 + count_bits_in_word(x); +} /** @param {string} str */ function toBits(str) { @@ -75,41 +97,19 @@ function toBits(str) { const BitArray = { /** * Concatenate two bit arrays. - * @param {bitArray} a1 The first array. - * @param {bitArray} a2 The second array. - * @return {bitArray} The concatenation of a1 and a2. - */ - concat: function (a1, a2) { - if (a1.length === 0 || a2.length === 0) { - return a1.concat(a2); - } - - var last = a1[a1.length - 1], - shift = BitArray.getPartial(last); - - if (shift === 32) { - return a1.concat(a2); - } else { - return BitArray._shiftRight(a2, shift, last | 0, a1.slice(0, a1.length - 1)); - } - }, - - /** - * Find the length of an array of bits. - * @param {bitArray} a The array. - * @return {number} The length of a, in bits. + * @param {bitArray} array The array. + * @param {number} n The number. + * @return {bitArray} The concatenation of a1 and n. */ - bitLength: function (a) { - var l = a.length, - x; + concat: function (array, n) { + if (array.length === 0) return [n]; - if (l === 0) { - return 0; - } + const last = array[array.length - 1]; + const shift = count_bits_in_word(last); - x = a[l - 1]; + if (shift === 32) return array.concat(n); - return (l - 1) * 32 + BitArray.getPartial(x); + return BitArray._shiftRight(n, shift, last, array.slice(0, array.length - 1)); }, /** @@ -127,27 +127,14 @@ const BitArray = { return (_end ? x | 0 : x << (32 - len)) + len * 0x10000000000; }, - /** - * Get the number of bits used by a partial word. - * @param {Number} x The partial word. - * @return {Number} The number of bits used by the partial word. - */ - getPartial: function (x) { - return Math.round(x / 0x10000000000) || 32; - }, - /** Shift an array right. - * @param {bitArray} a The array to shift. + * @param {number} a The array to shift. * @param {number} shift The number of bits to shift. * @param {number} [carry] A byte to carry in * @param {bitArray} [out] An array to prepend to the output. * @private */ _shiftRight: function (a, shift, carry = 0, out = []) { - var i, - last2 = 0, - shift2; - for (; shift >= 32; shift -= 32) { out.push(carry); @@ -158,15 +145,11 @@ const BitArray = { return out.concat(a); } - for (i = 0; i < a.length; i++) { - out.push(carry | (a[i] >>> shift)); + out.push(carry | (a >>> shift)); + carry = a << (32 - shift); - carry = a[i] << (32 - shift); - } - - last2 = a.length ? a[a.length - 1] : 0; - - shift2 = BitArray.getPartial(last2); + const last2 = a; + const shift2 = count_bits_in_word(last2); out.push( BitArray.partial( @@ -337,12 +320,19 @@ export function hash(data) { const out = init.slice(0); + let uint8array = encoder.encode(data); + let l = 4 * Math.ceil(uint8array.length / 4); + if (uint8array.length < l) { + const tmp = new Uint8Array(l); + tmp.set(uint8array); + uint8array = tmp; + } /** @type {bitArray} */ let buffer = toBits(data); - let length = BitArray.bitLength(buffer); + let bits = count_bits_in_buffer(buffer); // Round out and push the buffer - buffer = BitArray.concat(buffer, [BitArray.partial(1, 1)]); + buffer = BitArray.concat(buffer, 0xff80000000); // Round out the buffer to a multiple of 16 words, less the 2 length words. for (let i = buffer.length + 2; i & 15; i++) { @@ -350,8 +340,8 @@ export function hash(data) { } // append the length - buffer.push(Math.floor(length / 0x100000000)); - buffer.push(length | 0); + buffer.push(Math.floor(bits / 0x100000000)); // this will always be zero for us + buffer.push(bits | 0); const uint32array = new Uint32Array(buffer); From 720ad9191bc98b8fd98533744d9199d6de44c952 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 15:23:57 -0500 Subject: [PATCH 36/60] more tidying --- .../src/runtime/server/page/crypto.spec.js | 3 +- packages/kit/src/runtime/server/page/sjcl.js | 69 ++++++++++++------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/packages/kit/src/runtime/server/page/crypto.spec.js b/packages/kit/src/runtime/server/page/crypto.spec.js index a46a84018462..115986537a86 100644 --- a/packages/kit/src/runtime/server/page/crypto.spec.js +++ b/packages/kit/src/runtime/server/page/crypto.spec.js @@ -4,7 +4,8 @@ import crypto from 'crypto'; import { sha256 } from './crypto.js'; const inputs = [ - 'abc', + '', + 'abcd', 'the quick brown fox jumps over the lazy dog', '工欲善其事,必先利其器' ].slice(0); diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index ee9d3838b073..16be9fe6412c 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -42,7 +42,7 @@ function count_bits_in_buffer(a) { } /** @param {string} str */ -function toBits(str) { +function to_bits(str) { let uint8array = encoder.encode(str); const out = []; @@ -97,19 +97,41 @@ function toBits(str) { const BitArray = { /** * Concatenate two bit arrays. - * @param {bitArray} array The array. - * @param {number} n The number. - * @return {bitArray} The concatenation of a1 and n. + * @param {bitArray} a1 The first array. + * @param {bitArray} a2 The second array. + * @return {bitArray} The concatenation of a1 and a2. */ - concat: function (array, n) { - if (array.length === 0) return [n]; + concat: function (a1, a2) { + if (a1.length === 0) { + return a1.concat(a2); + } - const last = array[array.length - 1]; + const last = a1[a1.length - 1]; const shift = count_bits_in_word(last); - if (shift === 32) return array.concat(n); + if (shift === 32) { + return a1.concat(a2); + } else { + return BitArray._shiftRight(a2, shift, last | 0, a1.slice(0, a1.length - 1)); + } + }, + + /** + * Find the length of an array of bits. + * @param {bitArray} a The array. + * @return {number} The length of a, in bits. + */ + bitLength: function (a) { + var l = a.length, + x; - return BitArray._shiftRight(n, shift, last, array.slice(0, array.length - 1)); + if (l === 0) { + return 0; + } + + x = a[l - 1]; + + return (l - 1) * 32 + count_bits_in_word(x); }, /** @@ -128,13 +150,17 @@ const BitArray = { }, /** Shift an array right. - * @param {number} a The array to shift. + * @param {bitArray} a The array to shift. * @param {number} shift The number of bits to shift. * @param {number} [carry] A byte to carry in * @param {bitArray} [out] An array to prepend to the output. * @private */ _shiftRight: function (a, shift, carry = 0, out = []) { + var i, + last2 = 0, + shift2; + for (; shift >= 32; shift -= 32) { out.push(carry); @@ -145,11 +171,15 @@ const BitArray = { return out.concat(a); } - out.push(carry | (a >>> shift)); - carry = a << (32 - shift); + for (i = 0; i < a.length; i++) { + out.push(carry | (a[i] >>> shift)); - const last2 = a; - const shift2 = count_bits_in_word(last2); + carry = a[i] << (32 - shift); + } + + last2 = a.length ? a[a.length - 1] : 0; + + shift2 = count_bits_in_word(last2); out.push( BitArray.partial( @@ -320,19 +350,12 @@ export function hash(data) { const out = init.slice(0); - let uint8array = encoder.encode(data); - let l = 4 * Math.ceil(uint8array.length / 4); - if (uint8array.length < l) { - const tmp = new Uint8Array(l); - tmp.set(uint8array); - uint8array = tmp; - } /** @type {bitArray} */ - let buffer = toBits(data); + let buffer = to_bits(data); let bits = count_bits_in_buffer(buffer); // Round out and push the buffer - buffer = BitArray.concat(buffer, 0xff80000000); + buffer = BitArray.concat(buffer, [0xff80000000]); // Round out the buffer to a multiple of 16 words, less the 2 length words. for (let i = buffer.length + 2; i & 15; i++) { From 1dc0a050ae1c12828117badf5d44bf549ec3d459 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 15:36:04 -0500 Subject: [PATCH 37/60] simplify --- packages/kit/src/runtime/server/page/sjcl.js | 25 ++++---------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 16be9fe6412c..e2bc2b058b69 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -28,24 +28,11 @@ function count_bits_in_word(n) { return Math.round(n / 0x10000000000) || 32; } -/** - * Find the length of an array of bits. - * @param {Uint32Array} a The array. - * @return {number} The length of a, in bits. - */ -function count_bits_in_buffer(a) { - const l = a.length; - if (l === 0) return 0; - - const x = a[l - 1]; - return (l - 1) * 32 + count_bits_in_word(x); -} - /** @param {string} str */ function to_bits(str) { let uint8array = encoder.encode(str); - const out = []; + const buffer = []; let i; let tmp = 0; @@ -54,17 +41,17 @@ function to_bits(str) { tmp = (tmp << 8) | uint8array[i]; if ((i & 3) === 3) { - out.push(tmp); + buffer.push(tmp); tmp = 0; } } if (i & 3) { let partial = BitArray.partial(8 * (i & 3), tmp); - out.push(partial); + buffer.push(partial); } - return out; + return { buffer, bits: uint8array.length * 8 }; } /** @@ -350,9 +337,7 @@ export function hash(data) { const out = init.slice(0); - /** @type {bitArray} */ - let buffer = to_bits(data); - let bits = count_bits_in_buffer(buffer); + let { buffer, bits } = to_bits(data); // Round out and push the buffer buffer = BitArray.concat(buffer, [0xff80000000]); From 2b3ca8fa857c742e5a1d126a323c52a0106cfc2f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 16:39:43 -0500 Subject: [PATCH 38/60] radically simplify --- .../src/runtime/server/page/crypto.spec.js | 1 + packages/kit/src/runtime/server/page/sjcl.js | 187 ++---------------- 2 files changed, 16 insertions(+), 172 deletions(-) diff --git a/packages/kit/src/runtime/server/page/crypto.spec.js b/packages/kit/src/runtime/server/page/crypto.spec.js index 115986537a86..cf967e184c97 100644 --- a/packages/kit/src/runtime/server/page/crypto.spec.js +++ b/packages/kit/src/runtime/server/page/crypto.spec.js @@ -4,6 +4,7 @@ import crypto from 'crypto'; import { sha256 } from './crypto.js'; const inputs = [ + 'hello world', '', 'abcd', 'the quick brown fox jumps over the lazy dog', diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index e2bc2b058b69..2e27bbd4eaad 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -1,6 +1,6 @@ const encoder = new TextEncoder(); -/** @param {Uint32Array} uint32array' */ +/** @param {Uint32Array} uint32array */ function swap_endianness(uint32array) { const uint8array = new Uint8Array(uint32array.buffer); @@ -19,167 +19,25 @@ function swap_endianness(uint32array) { return uint32array; } -/** - * Get the number of bits used by a partial word. - * @param {number} n The partial word. - * @return {number} The number of bits used by the partial word. - */ -function count_bits_in_word(n) { - return Math.round(n / 0x10000000000) || 32; -} - /** @param {string} str */ function to_bits(str) { - let uint8array = encoder.encode(str); + const encoded = encoder.encode(str); + const length = encoded.length * 8; - const buffer = []; + const size = 512 * Math.ceil((length + 129) / 512); + const bytes = new Uint8Array(size / 8); + bytes.set(encoded); + bytes[encoded.length] = 0b10000000; - let i; - let tmp = 0; + swap_endianness(new Uint32Array(bytes.buffer)); - for (i = 0; i < uint8array.length; i++) { - tmp = (tmp << 8) | uint8array[i]; - - if ((i & 3) === 3) { - buffer.push(tmp); - tmp = 0; - } - } + const words = new Uint32Array(bytes.buffer); + words[words.length - 2] = Math.floor(length / 0x100000000); // this will always be zero for us + words[words.length - 1] = length; - if (i & 3) { - let partial = BitArray.partial(8 * (i & 3), tmp); - buffer.push(partial); - } - - return { buffer, bits: uint8array.length * 8 }; + return words; } -/** - * Arrays of bits, encoded as arrays of Numbers. - * @namespace - * @description - *

- * These objects are the currency accepted by SJCL's crypto functions. - *

- * - *

- * Most of our crypto primitives operate on arrays of 4-byte words internally, - * but many of them can take arguments that are not a multiple of 4 bytes. - * This library encodes arrays of bits (whose size need not be a multiple of 8 - * bits) as arrays of 32-bit words. The bits are packed, big-endian, into an - * array of words, 32 bits at a time. Since the words are double-precision - * floating point numbers, they fit some extra data. We use this (in a private, - * possibly-changing manner) to encode the number of bits actually present - * in the last word of the array. - *

- * - *

- * Because bitwise ops clear this out-of-band data, these arrays can be passed - * to ciphers like AES which want arrays of words. - *

- */ - -/** @typedef {number[]} bitArray */ - -const BitArray = { - /** - * Concatenate two bit arrays. - * @param {bitArray} a1 The first array. - * @param {bitArray} a2 The second array. - * @return {bitArray} The concatenation of a1 and a2. - */ - concat: function (a1, a2) { - if (a1.length === 0) { - return a1.concat(a2); - } - - const last = a1[a1.length - 1]; - const shift = count_bits_in_word(last); - - if (shift === 32) { - return a1.concat(a2); - } else { - return BitArray._shiftRight(a2, shift, last | 0, a1.slice(0, a1.length - 1)); - } - }, - - /** - * Find the length of an array of bits. - * @param {bitArray} a The array. - * @return {number} The length of a, in bits. - */ - bitLength: function (a) { - var l = a.length, - x; - - if (l === 0) { - return 0; - } - - x = a[l - 1]; - - return (l - 1) * 32 + count_bits_in_word(x); - }, - - /** - * Make a partial word for a bit array. - * @param {Number} len The number of bits in the word. - * @param {Number} x The bits. - * @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side. - * @return {Number} The partial word. - */ - partial: function (len, x, _end) { - if (len === 32) { - return x; - } - - return (_end ? x | 0 : x << (32 - len)) + len * 0x10000000000; - }, - - /** Shift an array right. - * @param {bitArray} a The array to shift. - * @param {number} shift The number of bits to shift. - * @param {number} [carry] A byte to carry in - * @param {bitArray} [out] An array to prepend to the output. - * @private - */ - _shiftRight: function (a, shift, carry = 0, out = []) { - var i, - last2 = 0, - shift2; - - for (; shift >= 32; shift -= 32) { - out.push(carry); - - carry = 0; - } - - if (shift === 0) { - return out.concat(a); - } - - for (i = 0; i < a.length; i++) { - out.push(carry | (a[i] >>> shift)); - - carry = a[i] << (32 - shift); - } - - last2 = a.length ? a[a.length - 1] : 0; - - shift2 = count_bits_in_word(last2); - - out.push( - BitArray.partial( - (shift + shift2) & 31, - shift + shift2 > 32 ? carry : /** @type {number} */ (out.pop()), - 1 - ) - ); - - return out; - } -}; - /** * The SHA-256 initialization vector, to be precomputed. */ @@ -336,25 +194,10 @@ export function hash(data) { if (!key[0]) precompute(); const out = init.slice(0); + const array = to_bits(data); - let { buffer, bits } = to_bits(data); - - // Round out and push the buffer - buffer = BitArray.concat(buffer, [0xff80000000]); - - // Round out the buffer to a multiple of 16 words, less the 2 length words. - for (let i = buffer.length + 2; i & 15; i++) { - buffer.push(0); - } - - // append the length - buffer.push(Math.floor(bits / 0x100000000)); // this will always be zero for us - buffer.push(bits | 0); - - const uint32array = new Uint32Array(buffer); - - for (let i = 0; i < uint32array.length; i += 16) { - block(out, uint32array.subarray(i, i + 16)); + for (let i = 0; i < array.length; i += 16) { + block(out, array.subarray(i, i + 16)); } return out; From 65be836569d4b4bdd67829e487de9e93734317d3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 16:48:16 -0500 Subject: [PATCH 39/60] simplify further --- packages/kit/src/runtime/server/page/sjcl.js | 273 ++++++++----------- 1 file changed, 121 insertions(+), 152 deletions(-) diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js index 2e27bbd4eaad..b26f49271cc5 100644 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ b/packages/kit/src/runtime/server/page/sjcl.js @@ -1,72 +1,12 @@ const encoder = new TextEncoder(); -/** @param {Uint32Array} uint32array */ -function swap_endianness(uint32array) { - const uint8array = new Uint8Array(uint32array.buffer); - - for (let i = 0; i < uint8array.length; i += 4) { - const a = uint8array[i + 0]; - const b = uint8array[i + 1]; - const c = uint8array[i + 2]; - const d = uint8array[i + 3]; - - uint8array[i + 0] = d; - uint8array[i + 1] = c; - uint8array[i + 2] = b; - uint8array[i + 3] = a; - } - - return uint32array; -} - -/** @param {string} str */ -function to_bits(str) { - const encoded = encoder.encode(str); - const length = encoded.length * 8; - - const size = 512 * Math.ceil((length + 129) / 512); - const bytes = new Uint8Array(size / 8); - bytes.set(encoded); - bytes[encoded.length] = 0b10000000; - - swap_endianness(new Uint32Array(bytes.buffer)); - - const words = new Uint32Array(bytes.buffer); - words[words.length - 2] = Math.floor(length / 0x100000000); // this will always be zero for us - words[words.length - 1] = length; - - return words; -} - -/** - * The SHA-256 initialization vector, to be precomputed. - */ +/** The SHA-256 initialization vector */ const init = new Uint32Array(8); -/* - * init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], - */ - -/** - * The SHA-256 hash key, to be precomputed. - */ +/** The SHA-256 hash key */ const key = new Uint32Array(64); -/* - * key: - * [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - * 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - * 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - * 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - * 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - * 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - * 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - * 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], - */ - -/** - * Function to precompute _init and _key. - */ +/** Function to precompute init and key. */ function precompute() { /** @param {number} x */ function frac(x) { @@ -98,106 +38,135 @@ function precompute() { } } -/** - * Perform one cycle of SHA-256. - * @param {Uint32Array} out - * @param {Uint32Array} w one block of words. - */ -const block = (out, w) => { - var tmp; - var a; - var b; - - let out0 = out[0]; - let out1 = out[1]; - let out2 = out[2]; - let out3 = out[3]; - let out4 = out[4]; - let out5 = out[5]; - let out6 = out[6]; - let out7 = out[7]; - - /* Rationale for placement of |0 : - * If a value can overflow is original 32 bits by a factor of more than a few - * million (2^23 ish), there is a possibility that it might overflow the - * 53-bit mantissa and lose precision. - * - * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that - * propagates around the loop, and on the hash state out[]. I don't believe - * that the clamps on out4 and on out0 are strictly necessary, but it's close - * (for out4 anyway), and better safe than sorry. - * - * The clamps on out[] are necessary for the output to be correct even in the - * common case and for short inputs. - */ - - for (let i = 0; i < 64; i++) { - // load up the input word for this round - - if (i < 16) { - tmp = w[i]; - } else { - a = w[(i + 1) & 15]; - - b = w[(i + 14) & 15]; - - tmp = w[i & 15] = - (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + - ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + - w[i & 15] + - w[(i + 9) & 15]) | - 0; - } +/** @param {string} str */ +function encode(str) { + const encoded = encoder.encode(str); + const length = encoded.length * 8; - tmp = - tmp + - out7 + - ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + - (out6 ^ (out4 & (out5 ^ out6))) + - key[i]; // | 0; - - // shift register - out7 = out6; - out6 = out5; - out5 = out4; - - out4 = (out3 + tmp) | 0; - - out3 = out2; - out2 = out1; - out1 = out0; - - out0 = - (tmp + - ((out1 & out2) ^ (out3 & (out1 ^ out2))) + - ((out1 >>> 2) ^ - (out1 >>> 13) ^ - (out1 >>> 22) ^ - (out1 << 30) ^ - (out1 << 19) ^ - (out1 << 10))) | - 0; + // result should be a multiple of 512 bits in length, + // with room for a 1 (after the data) and two 32-bit + // words containing the original input bit length + const size = 512 * Math.ceil((length + 65) / 512); + const bytes = new Uint8Array(size / 8); + bytes.set(encoded); + + // append a 1 + bytes[encoded.length] = 0b10000000; + + // swap endianness + for (let i = 0; i < bytes.length; i += 4) { + const a = bytes[i + 0]; + const b = bytes[i + 1]; + const c = bytes[i + 2]; + const d = bytes[i + 3]; + + bytes[i + 0] = d; + bytes[i + 1] = c; + bytes[i + 2] = b; + bytes[i + 3] = a; } - out[0] = (out[0] + out0) | 0; - out[1] = (out[1] + out1) | 0; - out[2] = (out[2] + out2) | 0; - out[3] = (out[3] + out3) | 0; - out[4] = (out[4] + out4) | 0; - out[5] = (out[5] + out5) | 0; - out[6] = (out[6] + out6) | 0; - out[7] = (out[7] + out7) | 0; -}; + // add the input bit length + const words = new Uint32Array(bytes.buffer); + words[words.length - 2] = Math.floor(length / 0x100000000); // this will always be zero for us + words[words.length - 1] = length; + + return words; +} /** @param {string} data */ export function hash(data) { if (!key[0]) precompute(); const out = init.slice(0); - const array = to_bits(data); + const array = encode(data); for (let i = 0; i < array.length; i += 16) { - block(out, array.subarray(i, i + 16)); + const w = array.subarray(i, i + 16); + + let tmp; + let a; + let b; + + let out0 = out[0]; + let out1 = out[1]; + let out2 = out[2]; + let out3 = out[3]; + let out4 = out[4]; + let out5 = out[5]; + let out6 = out[6]; + let out7 = out[7]; + + /* Rationale for placement of |0 : + * If a value can overflow is original 32 bits by a factor of more than a few + * million (2^23 ish), there is a possibility that it might overflow the + * 53-bit mantissa and lose precision. + * + * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that + * propagates around the loop, and on the hash state out[]. I don't believe + * that the clamps on out4 and on out0 are strictly necessary, but it's close + * (for out4 anyway), and better safe than sorry. + * + * The clamps on out[] are necessary for the output to be correct even in the + * common case and for short inputs. + */ + + for (let i = 0; i < 64; i++) { + // load up the input word for this round + + if (i < 16) { + tmp = w[i]; + } else { + a = w[(i + 1) & 15]; + + b = w[(i + 14) & 15]; + + tmp = w[i & 15] = + (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + + ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + + w[i & 15] + + w[(i + 9) & 15]) | + 0; + } + + tmp = + tmp + + out7 + + ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + + (out6 ^ (out4 & (out5 ^ out6))) + + key[i]; // | 0; + + // shift register + out7 = out6; + out6 = out5; + out5 = out4; + + out4 = (out3 + tmp) | 0; + + out3 = out2; + out2 = out1; + out1 = out0; + + out0 = + (tmp + + ((out1 & out2) ^ (out3 & (out1 ^ out2))) + + ((out1 >>> 2) ^ + (out1 >>> 13) ^ + (out1 >>> 22) ^ + (out1 << 30) ^ + (out1 << 19) ^ + (out1 << 10))) | + 0; + } + + out[0] = (out[0] + out0) | 0; + out[1] = (out[1] + out1) | 0; + out[2] = (out[2] + out2) | 0; + out[3] = (out[3] + out3) | 0; + out[4] = (out[4] + out4) | 0; + out[5] = (out[5] + out5) | 0; + out[6] = (out[6] + out6) | 0; + out[7] = (out[7] + out7) | 0; } return out; From 3126da02c6e797037b42d4da8c16a7e94080aebc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 17:38:28 -0500 Subject: [PATCH 40/60] more crypto stuff --- packages/adapter-netlify/src/shims.js | 2 - packages/adapter-node/src/shims.js | 2 - packages/adapter-vercel/files/shims.js | 2 - packages/kit/package.json | 3 - packages/kit/rollup.config.js | 1 - .../kit/src/core/adapt/prerender/prerender.js | 2 - packages/kit/src/core/dev/plugin.js | 2 - packages/kit/src/core/preview/index.js | 2 - packages/kit/src/install-crypto.js | 12 - .../kit/src/runtime/server/page/crypto.js | 205 ++++++++++++++++-- packages/kit/src/runtime/server/page/csp.js | 46 ++-- .../kit/src/runtime/server/page/render.js | 3 +- packages/kit/src/runtime/server/page/sjcl.js | 173 --------------- 13 files changed, 208 insertions(+), 247 deletions(-) delete mode 100644 packages/kit/src/install-crypto.js delete mode 100644 packages/kit/src/runtime/server/page/sjcl.js diff --git a/packages/adapter-netlify/src/shims.js b/packages/adapter-netlify/src/shims.js index 2e7f455d2b07..8bd70fe8b758 100644 --- a/packages/adapter-netlify/src/shims.js +++ b/packages/adapter-netlify/src/shims.js @@ -1,5 +1,3 @@ -import { install_crypto } from '@sveltejs/kit/install-crypto'; import { install_fetch } from '@sveltejs/kit/install-fetch'; -install_crypto(); install_fetch(); diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js index 2e7f455d2b07..8bd70fe8b758 100644 --- a/packages/adapter-node/src/shims.js +++ b/packages/adapter-node/src/shims.js @@ -1,5 +1,3 @@ -import { install_crypto } from '@sveltejs/kit/install-crypto'; import { install_fetch } from '@sveltejs/kit/install-fetch'; -install_crypto(); install_fetch(); diff --git a/packages/adapter-vercel/files/shims.js b/packages/adapter-vercel/files/shims.js index 2e7f455d2b07..8bd70fe8b758 100644 --- a/packages/adapter-vercel/files/shims.js +++ b/packages/adapter-vercel/files/shims.js @@ -1,5 +1,3 @@ -import { install_crypto } from '@sveltejs/kit/install-crypto'; import { install_fetch } from '@sveltejs/kit/install-fetch'; -install_crypto(); install_fetch(); diff --git a/packages/kit/package.json b/packages/kit/package.json index 9042aebc85b9..9bdf36eb11c8 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -86,9 +86,6 @@ "./hooks": { "import": "./dist/hooks.js" }, - "./install-crypto": { - "import": "./dist/install-crypto.js" - }, "./install-fetch": { "import": "./dist/install-fetch.js" } diff --git a/packages/kit/rollup.config.js b/packages/kit/rollup.config.js index 1da2dd0fbee9..9361bebb710b 100644 --- a/packages/kit/rollup.config.js +++ b/packages/kit/rollup.config.js @@ -57,7 +57,6 @@ export default [ cli: 'src/cli.js', node: 'src/node.js', hooks: 'src/hooks.js', - 'install-crypto': 'src/install-crypto.js', 'install-fetch': 'src/install-fetch.js' }, output: { diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js index 5bc1ec76ad1f..368a8d01d6de 100644 --- a/packages/kit/src/core/adapt/prerender/prerender.js +++ b/packages/kit/src/core/adapt/prerender/prerender.js @@ -3,7 +3,6 @@ import { dirname, join, resolve as resolve_path } from 'path'; import { pathToFileURL, URL } from 'url'; import { mkdirp } from '../../../utils/filesystem.js'; import { install_fetch } from '../../../install-fetch.js'; -import { install_crypto } from '../../../install-crypto.js'; import { SVELTE_KIT } from '../../constants.js'; import { is_root_relative, resolve } from '../../../utils/url.js'; import { queue } from './queue.js'; @@ -58,7 +57,6 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a } install_fetch(); - install_crypto(); mkdirp(out); diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js index a50ca589b79e..8aa8560c83c0 100644 --- a/packages/kit/src/core/dev/plugin.js +++ b/packages/kit/src/core/dev/plugin.js @@ -5,7 +5,6 @@ import colors from 'kleur'; import sirv from 'sirv'; import { respond } from '../../runtime/server/index.js'; import { install_fetch } from '../../install-fetch.js'; -import { install_crypto } from '../../install-crypto.js'; import { create_app } from '../create_app/index.js'; import create_manifest_data from '../create_manifest_data/index.js'; import { getRequest, setResponse } from '../../node.js'; @@ -33,7 +32,6 @@ export async function create_plugin(config, cwd) { configureServer(vite) { install_fetch(); - install_crypto(); /** @type {import('types/app').SSRManifest} */ let manifest; diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index 10f0565177ab..7136edd96a55 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -6,7 +6,6 @@ import sirv from 'sirv'; import { pathToFileURL } from 'url'; import { getRequest, setResponse } from '../../node.js'; import { install_fetch } from '../../install-fetch.js'; -import { install_crypto } from '../../install-crypto.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; /** @param {string} dir */ @@ -33,7 +32,6 @@ export async function preview({ cwd = process.cwd() }) { install_fetch(); - install_crypto(); const app_file = resolve(cwd, `${SVELTE_KIT}/output/server/app.js`); const manifest_file = resolve(cwd, `${SVELTE_KIT}/output/server/manifest.js`); diff --git a/packages/kit/src/install-crypto.js b/packages/kit/src/install-crypto.js deleted file mode 100644 index 9eee91b082f5..000000000000 --- a/packages/kit/src/install-crypto.js +++ /dev/null @@ -1,12 +0,0 @@ -import { webcrypto } from 'crypto'; - -// exported for dev/preview and node environments -export function install_crypto() { - Object.defineProperties(globalThis, { - crypto: { - enumerable: true, - configurable: true, - value: webcrypto - } - }); -} diff --git a/packages/kit/src/runtime/server/page/crypto.js b/packages/kit/src/runtime/server/page/crypto.js index ebceeb5e2902..4c6998a8f38a 100644 --- a/packages/kit/src/runtime/server/page/crypto.js +++ b/packages/kit/src/runtime/server/page/crypto.js @@ -1,29 +1,186 @@ -import { hash } from './sjcl.js'; - -// adapted from https://bitwiseshiftleft.github.io/sjcl/, -// modified and redistributed under BSD license - -/** @param {string} text */ -export function sha256(text) { - // const hashed = new Uint32Array(Sha256.hash(text)); - const hashed = new Uint32Array(hash(text)); - const buffer = hashed.buffer; - const uint8array = new Uint8Array(buffer); - - // bitArray is big endian, uint32array is little endian, so we need to do this: - for (let i = 0; i < uint8array.length; i += 4) { - const a = uint8array[i + 0]; - const b = uint8array[i + 1]; - const c = uint8array[i + 2]; - const d = uint8array[i + 3]; - - uint8array[i + 0] = d; - uint8array[i + 1] = c; - uint8array[i + 2] = b; - uint8array[i + 3] = a; +const encoder = new TextEncoder(); + +/** + * SHA-256 hashing function adapted from https://bitwiseshiftleft.github.io/sjcl + * modified and redistributed under BSD license + * @param {string} data + */ +export function sha256(data) { + if (!key[0]) precompute(); + + const out = init.slice(0); + const array = encode(data); + + for (let i = 0; i < array.length; i += 16) { + const w = array.subarray(i, i + 16); + + let tmp; + let a; + let b; + + let out0 = out[0]; + let out1 = out[1]; + let out2 = out[2]; + let out3 = out[3]; + let out4 = out[4]; + let out5 = out[5]; + let out6 = out[6]; + let out7 = out[7]; + + /* Rationale for placement of |0 : + * If a value can overflow is original 32 bits by a factor of more than a few + * million (2^23 ish), there is a possibility that it might overflow the + * 53-bit mantissa and lose precision. + * + * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that + * propagates around the loop, and on the hash state out[]. I don't believe + * that the clamps on out4 and on out0 are strictly necessary, but it's close + * (for out4 anyway), and better safe than sorry. + * + * The clamps on out[] are necessary for the output to be correct even in the + * common case and for short inputs. + */ + + for (let i = 0; i < 64; i++) { + // load up the input word for this round + + if (i < 16) { + tmp = w[i]; + } else { + a = w[(i + 1) & 15]; + + b = w[(i + 14) & 15]; + + tmp = w[i & 15] = + (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + + ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + + w[i & 15] + + w[(i + 9) & 15]) | + 0; + } + + tmp = + tmp + + out7 + + ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + + (out6 ^ (out4 & (out5 ^ out6))) + + key[i]; // | 0; + + // shift register + out7 = out6; + out6 = out5; + out5 = out4; + + out4 = (out3 + tmp) | 0; + + out3 = out2; + out2 = out1; + out1 = out0; + + out0 = + (tmp + + ((out1 & out2) ^ (out3 & (out1 ^ out2))) + + ((out1 >>> 2) ^ + (out1 >>> 13) ^ + (out1 >>> 22) ^ + (out1 << 30) ^ + (out1 << 19) ^ + (out1 << 10))) | + 0; + } + + out[0] = (out[0] + out0) | 0; + out[1] = (out[1] + out1) | 0; + out[2] = (out[2] + out2) | 0; + out[3] = (out[3] + out3) | 0; + out[4] = (out[4] + out4) | 0; + out[5] = (out[5] + out5) | 0; + out[6] = (out[6] + out6) | 0; + out[7] = (out[7] + out7) | 0; + } + + const bytes = new Uint8Array(out.buffer); + reverse_endianness(bytes); + + return base64(bytes); +} + +/** The SHA-256 initialization vector */ +const init = new Uint32Array(8); + +/** The SHA-256 hash key */ +const key = new Uint32Array(64); + +/** Function to precompute init and key. */ +function precompute() { + /** @param {number} x */ + function frac(x) { + return (x - Math.floor(x)) * 0x100000000; } - return base64(uint8array); + let prime = 2; + + for (let i = 0; i < 64; prime++) { + let is_prime = true; + + for (let factor = 2; factor * factor <= prime; factor++) { + if (prime % factor === 0) { + is_prime = false; + + break; + } + } + + if (is_prime) { + if (i < 8) { + init[i] = frac(prime ** (1 / 2)); + } + + key[i] = frac(prime ** (1 / 3)); + + i++; + } + } +} + +/** @param {Uint8Array} bytes */ +function reverse_endianness(bytes) { + for (let i = 0; i < bytes.length; i += 4) { + const a = bytes[i + 0]; + const b = bytes[i + 1]; + const c = bytes[i + 2]; + const d = bytes[i + 3]; + + bytes[i + 0] = d; + bytes[i + 1] = c; + bytes[i + 2] = b; + bytes[i + 3] = a; + } +} + +/** @param {string} str */ +function encode(str) { + const encoded = encoder.encode(str); + const length = encoded.length * 8; + + // result should be a multiple of 512 bits in length, + // with room for a 1 (after the data) and two 32-bit + // words containing the original input bit length + const size = 512 * Math.ceil((length + 65) / 512); + const bytes = new Uint8Array(size / 8); + bytes.set(encoded); + + // append a 1 + bytes[encoded.length] = 0b10000000; + + reverse_endianness(bytes); + + // add the input bit length + const words = new Uint32Array(bytes.buffer); + words[words.length - 2] = Math.floor(length / 0x100000000); // this will always be zero for us + words[words.length - 1] = length; + + return words; } /* diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 4b9b6da58175..57a31a7926ec 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -1,21 +1,25 @@ import { escape_html_attr } from '../../../utils/escape.js'; - -const array = new Uint8Array(16); - -export function generate_nonce() { - crypto.getRandomValues(array); - return base64(array); -} - -/** - * @param {string} contents - * @param {string} algorithm - * @returns - */ -async function generate_hash(contents, algorithm = 'sha-256') { - const bytes = new TextEncoder().encode(contents); - const digest = new Uint8Array(await crypto.subtle.digest(algorithm, bytes)); - return base64(digest); +import { sha256 } from './crypto.js'; + +/** @type {Promise} */ +export let csp_ready; + +/** @type {() => string} */ +let generate_nonce; + +if (typeof crypto !== 'undefined') { + const array = new Uint8Array(16); + + generate_nonce = () => { + crypto.getRandomValues(array); + return base64(array); + }; +} else { + csp_ready = import('crypto').then((crypto) => { + generate_nonce = () => { + return crypto.randomBytes(16).toString('base64'); + }; + }); } const quoted = new Set([ @@ -78,10 +82,10 @@ export class Csp { // TODO would be great if these methods weren't async /** @param {string} content */ - async add_script(content) { + add_script(content) { if (this.script_needs_csp) { if (this.#use_hashes) { - this.#script_src.push(`sha256-${await generate_hash(content)}`); + this.#script_src.push(`sha256-${sha256(content)}`); } else if (this.#script_src.length === 0) { this.#script_src.push(`nonce-${this.nonce}`); } @@ -89,10 +93,10 @@ export class Csp { } /** @param {string} content */ - async add_style(content) { + add_style(content) { if (this.style_needs_csp) { if (this.#use_hashes) { - this.#style_src.push(`sha256-${await generate_hash(content)}`); + this.#style_src.push(`sha256-${sha256(content)}`); } else if (this.#style_src.length === 0) { this.#style_src.push(`nonce-${this.nonce}`); } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 39a13eec5534..456f1dffbd15 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -5,7 +5,7 @@ import { hash } from '../../hash.js'; import { escape_html_attr } from '../../../utils/escape.js'; import { s } from '../../../utils/misc.js'; import { create_prerendering_url_proxy } from './utils.js'; -import { Csp } from './csp.js'; +import { Csp, csp_ready } from './csp.js'; // TODO rename this function/module @@ -139,6 +139,7 @@ export async function render_response({ const inlined_style = Array.from(styles.values()).join('\n'); + await csp_ready; const csp = new Csp(options.csp, !!state.prerender); // prettier-ignore diff --git a/packages/kit/src/runtime/server/page/sjcl.js b/packages/kit/src/runtime/server/page/sjcl.js deleted file mode 100644 index b26f49271cc5..000000000000 --- a/packages/kit/src/runtime/server/page/sjcl.js +++ /dev/null @@ -1,173 +0,0 @@ -const encoder = new TextEncoder(); - -/** The SHA-256 initialization vector */ -const init = new Uint32Array(8); - -/** The SHA-256 hash key */ -const key = new Uint32Array(64); - -/** Function to precompute init and key. */ -function precompute() { - /** @param {number} x */ - function frac(x) { - return (x - Math.floor(x)) * 0x100000000; - } - - let prime = 2; - - for (let i = 0; i < 64; prime++) { - let is_prime = true; - - for (let factor = 2; factor * factor <= prime; factor++) { - if (prime % factor === 0) { - is_prime = false; - - break; - } - } - - if (is_prime) { - if (i < 8) { - init[i] = frac(prime ** (1 / 2)); - } - - key[i] = frac(prime ** (1 / 3)); - - i++; - } - } -} - -/** @param {string} str */ -function encode(str) { - const encoded = encoder.encode(str); - const length = encoded.length * 8; - - // result should be a multiple of 512 bits in length, - // with room for a 1 (after the data) and two 32-bit - // words containing the original input bit length - const size = 512 * Math.ceil((length + 65) / 512); - const bytes = new Uint8Array(size / 8); - bytes.set(encoded); - - // append a 1 - bytes[encoded.length] = 0b10000000; - - // swap endianness - for (let i = 0; i < bytes.length; i += 4) { - const a = bytes[i + 0]; - const b = bytes[i + 1]; - const c = bytes[i + 2]; - const d = bytes[i + 3]; - - bytes[i + 0] = d; - bytes[i + 1] = c; - bytes[i + 2] = b; - bytes[i + 3] = a; - } - - // add the input bit length - const words = new Uint32Array(bytes.buffer); - words[words.length - 2] = Math.floor(length / 0x100000000); // this will always be zero for us - words[words.length - 1] = length; - - return words; -} - -/** @param {string} data */ -export function hash(data) { - if (!key[0]) precompute(); - - const out = init.slice(0); - const array = encode(data); - - for (let i = 0; i < array.length; i += 16) { - const w = array.subarray(i, i + 16); - - let tmp; - let a; - let b; - - let out0 = out[0]; - let out1 = out[1]; - let out2 = out[2]; - let out3 = out[3]; - let out4 = out[4]; - let out5 = out[5]; - let out6 = out[6]; - let out7 = out[7]; - - /* Rationale for placement of |0 : - * If a value can overflow is original 32 bits by a factor of more than a few - * million (2^23 ish), there is a possibility that it might overflow the - * 53-bit mantissa and lose precision. - * - * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that - * propagates around the loop, and on the hash state out[]. I don't believe - * that the clamps on out4 and on out0 are strictly necessary, but it's close - * (for out4 anyway), and better safe than sorry. - * - * The clamps on out[] are necessary for the output to be correct even in the - * common case and for short inputs. - */ - - for (let i = 0; i < 64; i++) { - // load up the input word for this round - - if (i < 16) { - tmp = w[i]; - } else { - a = w[(i + 1) & 15]; - - b = w[(i + 14) & 15]; - - tmp = w[i & 15] = - (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + - ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + - w[i & 15] + - w[(i + 9) & 15]) | - 0; - } - - tmp = - tmp + - out7 + - ((out4 >>> 6) ^ (out4 >>> 11) ^ (out4 >>> 25) ^ (out4 << 26) ^ (out4 << 21) ^ (out4 << 7)) + - (out6 ^ (out4 & (out5 ^ out6))) + - key[i]; // | 0; - - // shift register - out7 = out6; - out6 = out5; - out5 = out4; - - out4 = (out3 + tmp) | 0; - - out3 = out2; - out2 = out1; - out1 = out0; - - out0 = - (tmp + - ((out1 & out2) ^ (out3 & (out1 ^ out2))) + - ((out1 >>> 2) ^ - (out1 >>> 13) ^ - (out1 >>> 22) ^ - (out1 << 30) ^ - (out1 << 19) ^ - (out1 << 10))) | - 0; - } - - out[0] = (out[0] + out0) | 0; - out[1] = (out[1] + out1) | 0; - out[2] = (out[2] + out2) | 0; - out[3] = (out[3] + out3) | 0; - out[4] = (out[4] + out4) | 0; - out[5] = (out[5] + out5) | 0; - out[6] = (out[6] + out6) | 0; - out[7] = (out[7] + out7) | 0; - } - - return out; -} From 969a77116ed64a3cadcb029fde206e8165a955ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 17:50:41 -0500 Subject: [PATCH 41/60] use node crypto module to generate hashes where possible --- packages/kit/src/runtime/server/page/csp.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 57a31a7926ec..0252020e8ee7 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -7,6 +7,9 @@ export let csp_ready; /** @type {() => string} */ let generate_nonce; +/** @type {(input: string) => string} */ +let generate_hash; + if (typeof crypto !== 'undefined') { const array = new Uint8Array(16); @@ -14,11 +17,17 @@ if (typeof crypto !== 'undefined') { crypto.getRandomValues(array); return base64(array); }; + + generate_hash = sha256; } else { csp_ready = import('crypto').then((crypto) => { generate_nonce = () => { return crypto.randomBytes(16).toString('base64'); }; + + generate_hash = (input) => { + return crypto.createHash('sha256').update(input, 'utf-8').digest().toString('base64'); + }; }); } @@ -85,7 +94,7 @@ export class Csp { add_script(content) { if (this.script_needs_csp) { if (this.#use_hashes) { - this.#script_src.push(`sha256-${sha256(content)}`); + this.#script_src.push(`sha256-${generate_hash(content)}`); } else if (this.#script_src.length === 0) { this.#script_src.push(`nonce-${this.nonce}`); } @@ -96,7 +105,7 @@ export class Csp { add_style(content) { if (this.style_needs_csp) { if (this.#use_hashes) { - this.#style_src.push(`sha256-${sha256(content)}`); + this.#style_src.push(`sha256-${generate_hash(content)}`); } else if (this.#style_src.length === 0) { this.#style_src.push(`nonce-${this.nonce}`); } From 6e31aa0574da5d9092e397cbd1aec6aa29294911 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 17:55:50 -0500 Subject: [PATCH 42/60] remove unnecessary awaits --- packages/kit/src/runtime/server/page/render.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 456f1dffbd15..763ef25dc058 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -194,7 +194,7 @@ export async function render_response({ if (options.dev) attributes.push(' data-svelte'); if (csp.style_needs_nonce) attributes.push(` nonce="${csp.nonce}"`); - await csp.add_style(inlined_style); + csp.add_style(inlined_style); head += `\n\t${inlined_style}`; } @@ -226,7 +226,7 @@ export async function render_response({ const attributes = ['type="module"']; - await csp.add_script(init_app); + csp.add_script(init_app); if (csp.script_needs_nonce) { attributes.push(`nonce="${csp.nonce}"`); @@ -247,7 +247,7 @@ export async function render_response({ if (options.service_worker) { // always include service worker unless it's turned off explicitly - await csp.add_script(init_service_worker); + csp.add_script(init_service_worker); head += ` ${init_service_worker}`; From 591663678938506ec4907704650f2c5262a622d4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 18:13:04 -0500 Subject: [PATCH 43/60] fix mutation bug --- packages/kit/src/runtime/server/page/csp.js | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 0252020e8ee7..648d3bb12f39 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -65,7 +65,7 @@ export class Csp { */ constructor({ mode, directives }, prerender) { this.#use_hashes = mode === 'hash' || (mode === 'auto' && prerender); - this.#directives = { ...directives }; + this.#directives = directives; this.#script_src = []; this.#style_src = []; @@ -120,31 +120,31 @@ export class Csp { // (specifically, Firefox appears to not ignore nonce-{nonce} directives // on default-src), so we ensure that script-src and style-src exist - if (this.#script_src.length > 0) { - if (!this.#directives['script-src']) { - this.#directives['script-src'] = [...(this.#directives['default-src'] || [])]; - } + const directives = { ...this.#directives }; - this.#directives['script-src'].push(...this.#script_src); + if (this.#script_src.length > 0) { + directives['script-src'] = [ + ...(directives['script-src'] || directives['default-src'] || []), + ...this.#script_src + ]; } if (this.#style_src.length > 0) { - if (!this.#directives['style-src']) { - this.#directives['style-src'] = [...(this.#directives['default-src'] || [])]; - } - - this.#directives['style-src'].push(...this.#style_src); + directives['style-src'] = [ + ...(directives['style-src'] || directives['default-src'] || []), + ...this.#style_src + ]; } - for (const key in this.#directives) { + for (const key in directives) { if (is_meta && (key === 'frame-ancestors' || key === 'report-uri' || key === 'sandbox')) { // these values cannot be used with a tag // TODO warn? continue; } - // @ts-expect-error gimme a break typescript, `key` is obviously a member of this.#directives - const value = /** @type {string[] | true} */ (this.#directives[key]); + // @ts-expect-error gimme a break typescript, `key` is obviously a member of directives + const value = /** @type {string[] | true} */ (directives[key]); if (!value) continue; From 17afa3ee0ecb6ec75808f552103ce15f0b111f93 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 18:17:09 -0500 Subject: [PATCH 44/60] trick esbuild --- packages/kit/src/runtime/server/page/csp.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 648d3bb12f39..8dc39168a8ef 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -20,7 +20,8 @@ if (typeof crypto !== 'undefined') { generate_hash = sha256; } else { - csp_ready = import('crypto').then((crypto) => { + const name = 'crypto'; // store in a variable to fool esbuild when adapters bundle kit + csp_ready = import(name).then((crypto) => { generate_nonce = () => { return crypto.randomBytes(16).toString('base64'); }; From cb0968e30dbaa0095fd45fd7c35650ff81019b7c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 18:21:59 -0500 Subject: [PATCH 45/60] windows fix, hopefully --- packages/kit/src/core/config/index.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 7f33e3452506..0ef2d5f9fe9d 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -51,12 +51,12 @@ const get_defaults = (prefix = '') => ({ } }, files: { - assets: prefix + 'static', - hooks: prefix + 'src/hooks', - lib: prefix + 'src/lib', - routes: prefix + 'src/routes', - serviceWorker: prefix + 'src/service-worker', - template: prefix + 'src/app.html' + assets: join(prefix, 'static'), + hooks: join(prefix, 'src/hooks'), + lib: join(prefix, 'src/lib'), + routes: join(prefix, 'src/routes'), + serviceWorker: join(prefix, 'src/service-worker'), + template: join(prefix, 'src/app.html') }, floc: false, headers: undefined, From 7587a35ce5518b13211fbf640f713b6c1557b357 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Jan 2022 18:46:09 -0500 Subject: [PATCH 46/60] add unsafe-inline styles in dev --- packages/kit/src/runtime/server/page/csp.js | 44 ++++++++++++++----- .../kit/src/runtime/server/page/render.js | 2 +- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js index 8dc39168a8ef..3ac8a918bc70 100644 --- a/packages/kit/src/runtime/server/page/csp.js +++ b/packages/kit/src/runtime/server/page/csp.js @@ -48,6 +48,15 @@ export class Csp { /** @type {boolean} */ #use_hashes; + /** @type {boolean} */ + #dev; + + /** @type {boolean} */ + #script_needs_csp; + + /** @type {boolean} */ + #style_needs_csp; + /** @type {import('types/csp').CspDirectives} */ #directives; @@ -62,11 +71,13 @@ export class Csp { * mode: string, * directives: import('types/csp').CspDirectives * }} opts + * @param {boolean} dev * @param {boolean} prerender */ - constructor({ mode, directives }, prerender) { + constructor({ mode, directives }, dev, prerender) { this.#use_hashes = mode === 'hash' || (mode === 'auto' && prerender); this.#directives = directives; + this.#dev = dev; this.#script_src = []; this.#style_src = []; @@ -74,16 +85,17 @@ export class Csp { const effective_script_src = directives['script-src'] || directives['default-src']; const effective_style_src = directives['style-src'] || directives['default-src']; - this.script_needs_csp = - effective_script_src && + this.#script_needs_csp = + !!effective_script_src && effective_script_src.filter((value) => value !== 'unsafe-inline').length > 0; - this.style_needs_csp = - effective_style_src && + this.#style_needs_csp = + !dev && + !!effective_style_src && effective_style_src.filter((value) => value !== 'unsafe-inline').length > 0; - this.script_needs_nonce = this.script_needs_csp && !this.#use_hashes; - this.style_needs_nonce = this.style_needs_csp && !this.#use_hashes; + this.script_needs_nonce = this.#script_needs_csp && !this.#use_hashes; + this.style_needs_nonce = this.#style_needs_csp && !this.#use_hashes; if (this.script_needs_nonce || this.style_needs_nonce) { this.nonce = generate_nonce(); @@ -93,7 +105,7 @@ export class Csp { // TODO would be great if these methods weren't async /** @param {string} content */ add_script(content) { - if (this.script_needs_csp) { + if (this.#script_needs_csp) { if (this.#use_hashes) { this.#script_src.push(`sha256-${generate_hash(content)}`); } else if (this.#script_src.length === 0) { @@ -104,7 +116,7 @@ export class Csp { /** @param {string} content */ add_style(content) { - if (this.style_needs_csp) { + if (this.#style_needs_csp) { if (this.#use_hashes) { this.#style_src.push(`sha256-${generate_hash(content)}`); } else if (this.#style_src.length === 0) { @@ -130,7 +142,19 @@ export class Csp { ]; } - if (this.#style_src.length > 0) { + if (this.#dev) { + const effective_style_src = directives['style-src'] || directives['default-src']; + + // in development, we need to be able to inject