Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions esbuild/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
type WebExtensionManifest,
} from './config';

const require = createRequire(import.meta.url);

Check failure on line 19 in esbuild/plugins.ts

View workflow job for this annotation

GitHub Actions / Lint

'require' is declared but its value is never read.

Check warning on line 19 in esbuild/plugins.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/correctness/noUnusedVariables

This variable require is unused.

export const getPlugins = ({
outDir,
Expand All @@ -34,14 +34,6 @@
// case. The JSPM crypto package is too large and not tree shakeable, so we
// don't use it.
nodeBuiltin({ exclude: ['crypto'] }),
{
name: 'crypto-for-extension',
setup(build) {
build.onResolve({ filter: /^crypto$/ }, () => ({
path: require.resolve('crypto-browserify'),
}));
},
} satisfies ESBuildPlugin,
ignorePackagePlugin([
/@apidevtools[/|\\]json-schema-ref-parser/,
/@interledger[/|\\]openapi/,
Expand Down
140 changes: 140 additions & 0 deletions src/utils/browserCrypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// src/utils/browserCrypto.ts
/**
* Minimal wrapper around the browser's Web Crypto API to replace
* `crypto-browserify` for browser builds.
*
* This file intentionally implements a small subset of the Node `crypto`
* API surface that the extension needs:
* - randomBytes(size): Uint8Array | Buffer (if Buffer is present)
* - createHash(algorithm): { update(data), digest(encoding?) } (digest is async)
* - subtle: proxy to crypto.subtle (for direct usages)
*
* If you need more features (HMAC, PBKDF2, RSA/ECDSA transforms), we can
* extend this wrapper. Any unsupported operations will throw a clear error.
*/

function ensureCryptoAvailable() {
if (typeof globalThis.crypto === 'undefined') {
throw new Error(
'Web Crypto API (globalThis.crypto) is not available in this environment.',
);
}
if (typeof globalThis.crypto.subtle === 'undefined') {
// Some very old browsers expose getRandomValues but not subtle
// — surface a descriptive error.
throw new Error('Web Crypto Subtle API is not available in this environment.');
}
}

function toUint8Array(data: unknown): Uint8Array {
if (data instanceof Uint8Array) return data;
if (data instanceof ArrayBuffer) return new Uint8Array(data);
if (typeof data === 'string') {
// treat as utf-8 string
return new TextEncoder().encode(data);
}
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
throw new TypeError('Unsupported data type for crypto operation');
}

function toHex(buffer: ArrayBuffer) {
const u8 = new Uint8Array(buffer);
let hex = '';
for (let i = 0; i < u8.length; i++) {
const h = u8[i].toString(16).padStart(2, '0');
hex += h;
}
return hex;
}

function toBufferMaybe(u8: Uint8Array) {
// If Buffer exists (node-like env), return a Buffer; otherwise return Uint8Array
// Note: many browser code paths accept Uint8Array; adapt as needed.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

Check warning on line 54 in src/utils/browserCrypto.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/suspicious/noTsIgnore

Unsafe use of the @ts-ignore directive found in this comment.
if (typeof (globalThis as any).Buffer !== 'undefined') {

Check warning on line 55 in src/utils/browserCrypto.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/suspicious/noExplicitAny

Unexpected any. Specify a different type.
// @ts-ignore

Check warning on line 56 in src/utils/browserCrypto.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/suspicious/noTsIgnore

Unsafe use of the @ts-ignore directive found in this comment.
return (globalThis as any).Buffer.from(u8);

Check warning on line 57 in src/utils/browserCrypto.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/suspicious/noExplicitAny

Unexpected any. Specify a different type.
}
return u8;
}

export function randomBytes(size: number): Uint8Array | unknown {
if (typeof size !== 'number' || size < 0) {
throw new TypeError('size must be a non-negative number');
}
if (typeof globalThis.crypto?.getRandomValues !== 'function') {
throw new Error('crypto.getRandomValues is not available in this environment.');
}
const u8 = new Uint8Array(size);
globalThis.crypto.getRandomValues(u8);
return toBufferMaybe(u8);
}

/**
* createHash(algorithm) — returns an object with update() and digest(encoding?)
* The digest() is async and returns a Promise which resolves:
* - if encoding === 'hex' -> string hex
* - otherwise -> Uint8Array (or Buffer if Buffer available)
*
* Supported algorithms: 'SHA-1', 'SHA-256', 'SHA-384', 'SHA-512' (case-insensitive)
*/
export function createHash(algorithm = 'sha256') {
const algo = algorithm.toUpperCase().replace('-', '');
const chunks: Uint8Array[] = [];

return {
update(data: unknown) {
chunks.push(toUint8Array(data));
return this;
},
async digest(encoding?: 'hex' | 'utf8' | undefined) {
ensureCryptoAvailable();
const totalLen = chunks.reduce((s, c) => s + c.byteLength, 0);
const buffer = new Uint8Array(totalLen);
let offset = 0;
for (const c of chunks) {
buffer.set(c, offset);
offset += c.byteLength;
}

// Map common algorithm names to WebCrypto acceptable ones
let webAlgo: AlgorithmIdentifier;
switch (algo) {
case 'SHA1':
webAlgo = 'SHA-1';
break;
case 'SHA256':
webAlgo = 'SHA-256';
break;
case 'SHA384':
webAlgo = 'SHA-384';
break;
case 'SHA512':
webAlgo = 'SHA-512';
break;
default:
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
}

const digest = await globalThis.crypto.subtle.digest(webAlgo, buffer);
if (encoding === 'hex') {
return toHex(digest);
}
return toBufferMaybe(new Uint8Array(digest));
},
};
}

/**
* Export subtle for direct usages (if any). This lets code that wants to
* call crypto.subtle directly do so as `import { subtle } from 'crypto'`
* (if they import the module default), or via the resolved `crypto` object.
*/
export const subtle = typeof globalThis.crypto !== 'undefined' ? globalThis.crypto.subtle : undefined;

export default {
randomBytes,
createHash,
subtle,
};
Loading