mirror of
https://github.com/kou029w/websri.git
synced 2025-01-18 08:05:13 +00:00
create subresourceintegrity
This commit is contained in:
parent
87a3f47c4e
commit
a7c7e41d6e
12 changed files with 6216 additions and 0 deletions
35
.github/workflows/release.yml
vendored
Normal file
35
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
name: release
|
||||||
|
run-name: "${{ inputs.version }}"
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Increment "major", "minor", "patch", or "pre*" version; or specify version'
|
||||||
|
default: patch
|
||||||
|
required: true
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm test
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
git config user.email "${{ github.actor }}@users.noreply.github.com"
|
||||||
|
git config user.name "${{ github.actor }}"
|
||||||
|
npm run release -- "${{ github.event.inputs.version }}"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
21
.github/workflows/test.yml
vendored
Normal file
21
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: test
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
nodejs:
|
||||||
|
name: Node.js v${{ matrix.node-version }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version:
|
||||||
|
- 18
|
||||||
|
- 20
|
||||||
|
- 22
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm test
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
dist
|
||||||
|
node_modules
|
15
.release-it.json
Normal file
15
.release-it.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/release-it@17/schema/release-it.json",
|
||||||
|
"git": {
|
||||||
|
"tagName": "v${version}"
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": true
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"@release-it/keep-a-changelog": {
|
||||||
|
"addUnreleased": true,
|
||||||
|
"addVersionUrl": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
50
README.md
Normal file
50
README.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# subresourceintegrity
|
||||||
|
|
||||||
|
[![NPM Version](https://img.shields.io/npm/v/subresourceintegrity)](https://www.npmjs.com/package/subresourceintegrity) [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue)](https://www.jsdocs.io/package/subresourceintegrity)
|
||||||
|
|
||||||
|
`subresourceintegrity` is a utility designed for Subresource Integrity that works across various web-interoperable runtimes, including Node.js, browsers, Cloudflare Workers, Deno, Bun, and others.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Install package:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# npm
|
||||||
|
npm install subresourceintegrity
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn add subresourceintegrity
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install subresourceintegrity
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install subresourceintegrity
|
||||||
|
```
|
||||||
|
|
||||||
|
[Integrity Metadata](https://www.w3.org/TR/SRI/#integrity-metadata):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createIntegrityMetadata } from "subresourceintegrity";
|
||||||
|
|
||||||
|
const res = new Response("Hello, world!");
|
||||||
|
const data = await res.arrayBuffer();
|
||||||
|
const integrityMetadata = await createIntegrityMetadata("sha256", data);
|
||||||
|
|
||||||
|
console.log(integrityMetadata.toString());
|
||||||
|
// sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
[API Reference](https://www.jsdocs.io/package/subresourceintegrity)
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### What is Subresource Integrity?
|
||||||
|
|
||||||
|
[Subresource Integrity (SRI)](https://www.w3.org/TR/SRI/) is a security feature that allows user agents to verify that a fetched resource has not been manipulated unexpectedly.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
`subresourceintegrity` is released under the MIT License.
|
5827
package-lock.json
generated
Normal file
5827
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
38
package.json
Normal file
38
package.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"name": "subresourceintegrity",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Subresource Integrity",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Kohei Watanabe <nebel@fogtype.com>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/kou029w/subresourceintegrity.git"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"default": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"prepublishOnly": "tsup",
|
||||||
|
"test": "tsup && node --test",
|
||||||
|
"release": "release-it --"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@release-it/keep-a-changelog": "5.0.0",
|
||||||
|
"@types/node": "22.5.2",
|
||||||
|
"release-it": "17.6.0",
|
||||||
|
"tsup": "8.2.4",
|
||||||
|
"typescript": "5.5.4"
|
||||||
|
}
|
||||||
|
}
|
4
renovate.json
Normal file
4
renovate.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:best-practices", ":automergeAll"]
|
||||||
|
}
|
167
src/index.ts
Normal file
167
src/index.ts
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/** Content Security Policy Level 2, section 4.2 */
|
||||||
|
export type HashAlgorithm = "sha256" | "sha384" | "sha512";
|
||||||
|
|
||||||
|
export const supportedHashAlgorithm: ReadonlyArray<HashAlgorithm> = [
|
||||||
|
"sha512",
|
||||||
|
"sha384",
|
||||||
|
"sha256",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const supportedHashAlgorithmName = {
|
||||||
|
sha256: "SHA-256",
|
||||||
|
sha384: "SHA-384",
|
||||||
|
sha512: "SHA-512",
|
||||||
|
} satisfies Record<HashAlgorithm, string>;
|
||||||
|
|
||||||
|
export type PrioritizedHash = "" | HashAlgorithm;
|
||||||
|
|
||||||
|
export function getPrioritizedHash(
|
||||||
|
a: HashAlgorithm,
|
||||||
|
b: HashAlgorithm,
|
||||||
|
): PrioritizedHash {
|
||||||
|
if (a === b) return "";
|
||||||
|
if (!supportedHashAlgorithm.includes(a)) return "";
|
||||||
|
if (!supportedHashAlgorithm.includes(b)) return "";
|
||||||
|
return a < b ? b : a;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IntegrityMetadataRegex =
|
||||||
|
/^(?<alg>sha256|sha384|sha512)-(?<val>(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)(?:[?](?<opts>[\x21-\x7e]*))?$/;
|
||||||
|
|
||||||
|
export const SeparatorRegex = /[^\x21-\x7e]+/;
|
||||||
|
|
||||||
|
/** Integrity Metadata */
|
||||||
|
export class IntegrityMetadata {
|
||||||
|
alg: PrioritizedHash;
|
||||||
|
val: string;
|
||||||
|
opt: string[];
|
||||||
|
|
||||||
|
constructor(integrity: string) {
|
||||||
|
const {
|
||||||
|
alg = "",
|
||||||
|
val = "",
|
||||||
|
opt,
|
||||||
|
} = IntegrityMetadataRegex.exec(integrity)?.groups ?? {};
|
||||||
|
|
||||||
|
Object.assign(this, {
|
||||||
|
alg,
|
||||||
|
val,
|
||||||
|
opt: opt?.split("?") ?? [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match(integrity: { alg: PrioritizedHash; val: string }): boolean {
|
||||||
|
return integrity.alg === this.alg && integrity.val === this.val;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return IntegrityMetadata.stringify(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): string {
|
||||||
|
return this.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static stringify(integrity: {
|
||||||
|
alg: PrioritizedHash;
|
||||||
|
val: string;
|
||||||
|
opt: string[];
|
||||||
|
}): string {
|
||||||
|
if (!integrity.alg) return "";
|
||||||
|
if (!integrity.val) return "";
|
||||||
|
if (!supportedHashAlgorithm.includes(integrity.alg)) return "";
|
||||||
|
return `${integrity.alg}-${[integrity.val, ...integrity.opt].join("?")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createIntegrityMetadata(
|
||||||
|
hashAlgorithm: HashAlgorithm,
|
||||||
|
data: ArrayBuffer,
|
||||||
|
opt: string[] = [],
|
||||||
|
): Promise<string> {
|
||||||
|
const alg = hashAlgorithm.toLowerCase() as HashAlgorithm;
|
||||||
|
|
||||||
|
if (!supportedHashAlgorithm.includes(alg)) return "";
|
||||||
|
|
||||||
|
const arrayBuffer = await crypto.subtle.digest(
|
||||||
|
supportedHashAlgorithmName[alg.toLowerCase()],
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
const val = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||||
|
|
||||||
|
return IntegrityMetadata.stringify({
|
||||||
|
alg,
|
||||||
|
val,
|
||||||
|
opt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subresource Integrity */
|
||||||
|
export class SubresourceIntegrity extends Map<
|
||||||
|
HashAlgorithm,
|
||||||
|
IntegrityMetadata
|
||||||
|
> {
|
||||||
|
getPrioritizedHash: (a: HashAlgorithm, b: HashAlgorithm) => PrioritizedHash;
|
||||||
|
|
||||||
|
constructor(integrity: string, options = { getPrioritizedHash }) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const integrityMetadata = integrity.split(SeparatorRegex);
|
||||||
|
|
||||||
|
for (const integrity of integrityMetadata.filter(Boolean)) {
|
||||||
|
const integrityMetadata = new IntegrityMetadata(integrity);
|
||||||
|
|
||||||
|
if (integrityMetadata.alg) {
|
||||||
|
this.set(integrityMetadata.alg, integrityMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getPrioritizedHash = options.getPrioritizedHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
get strongest(): IntegrityMetadata {
|
||||||
|
const [hashAlgorithm = supportedHashAlgorithm[0]]: HashAlgorithm[] = [
|
||||||
|
...this.keys(),
|
||||||
|
].sort((a, b) => {
|
||||||
|
switch (this.getPrioritizedHash(a, b)) {
|
||||||
|
default:
|
||||||
|
case "":
|
||||||
|
return 0;
|
||||||
|
case a:
|
||||||
|
return -1;
|
||||||
|
case b:
|
||||||
|
return +1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.get(hashAlgorithm) ?? new IntegrityMetadata("");
|
||||||
|
}
|
||||||
|
|
||||||
|
match(integrity: { alg: PrioritizedHash; val: string }): boolean {
|
||||||
|
return this.strongest.match(integrity);
|
||||||
|
}
|
||||||
|
|
||||||
|
join(separator = " ") {
|
||||||
|
return [...this.values()].map(String).join(separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): string {
|
||||||
|
return this.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSubresourceIntegrity(
|
||||||
|
hashAlgorithms: HashAlgorithm[],
|
||||||
|
data: ArrayBuffer,
|
||||||
|
): Promise<SubresourceIntegrity> {
|
||||||
|
const integrityMetadata = await Promise.all(
|
||||||
|
hashAlgorithms.map((alg) => createIntegrityMetadata(alg, data)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new SubresourceIntegrity(integrityMetadata.join(" "));
|
||||||
|
}
|
39
test/index.js
Normal file
39
test/index.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import {
|
||||||
|
createIntegrityMetadata,
|
||||||
|
createSubresourceIntegrity,
|
||||||
|
SubresourceIntegrity,
|
||||||
|
} from "../dist/index.js";
|
||||||
|
|
||||||
|
test("createIntegrityMetadata()", async function () {
|
||||||
|
const res = new Response("Hello, world!");
|
||||||
|
const data = await res.arrayBuffer();
|
||||||
|
const integrityMetadata = await createIntegrityMetadata("sha256", data);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
integrityMetadata.toString(),
|
||||||
|
"sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createSubresourceIntegrity()", async function () {
|
||||||
|
const res = new Response("Hello, world!");
|
||||||
|
const data = await res.arrayBuffer();
|
||||||
|
const sri = await createSubresourceIntegrity(["sha384", "sha512"], data);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
sri.toString(),
|
||||||
|
"sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SubresourceIntegrity.strongest", async function () {
|
||||||
|
const { strongest } = new SubresourceIntegrity(`
|
||||||
|
sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=
|
||||||
|
sha384-VbxVaw0v4Pzlgrpf4Huq//A1ZTY4x6wNVJTCpkwL6hzFczHHwSpFzbyn9MNKCJ7r
|
||||||
|
sha512-wVJ82JPBJHc9gRkRlwyP5uhX1t9dySJr2KFgYUwM2WOk3eorlLt9NgIe+dhl1c6ilKgt1JoLsmn1H256V/eUIQ==
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.strictEqual(strongest.alg, "sha512");
|
||||||
|
});
|
10
tsup.config.ts
Normal file
10
tsup.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from "tsup";
|
||||||
|
|
||||||
|
export default defineConfig(() => {
|
||||||
|
return {
|
||||||
|
clean: true,
|
||||||
|
dts: true,
|
||||||
|
entry: ["src"],
|
||||||
|
format: ["cjs", "esm"],
|
||||||
|
};
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue