Skip to content

Commit e3f5f81

Browse files
authored
Use fnv1a instead of SHA256 to create images signatures (#2353)
1 parent f5df40e commit e3f5f81

File tree

4 files changed

+34
-5
lines changed

4 files changed

+34
-5
lines changed

bun.lockb

371 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@radix-ui/react-checkbox": "^1.0.4",
2525
"@radix-ui/react-popover": "^1.0.7",
2626
"@sentry/nextjs": "^7.94.1",
27+
"@sindresorhus/fnv1a": "^3.1.0",
2728
"@tailwindcss/container-queries": "^0.1.1",
2829
"@tailwindcss/typography": "^0.5.10",
2930
"@upstash/redis": "^1.27.1",

src/app/(global)/~gitbook/image/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export const runtime = 'edge';
1313
export async function GET(request: NextRequest) {
1414
let urlParam = request.nextUrl.searchParams.get('url');
1515
const signature = request.nextUrl.searchParams.get('sign');
16+
// The current signature algorithm sets version as 1, but we need to support the older version as well
17+
// for previously generated content. In this case, we default to version 0.
18+
const signatureVersion = (request.nextUrl.searchParams.get('sv') as '1') || '0';
1619
if (!urlParam || !signature) {
1720
return new Response('Missing url/sign parameters', { status: 400 });
1821
}
@@ -25,7 +28,7 @@ export async function GET(request: NextRequest) {
2528
}
2629

2730
// Verify the signature
28-
const verified = await verifyImageSignature(url, signature);
31+
const verified = await verifyImageSignature(url, { signature, version: signatureVersion });
2932
if (!verified) {
3033
return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 });
3134
}

src/lib/images.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'server-only';
22

3+
import fnv1a from '@sindresorhus/fnv1a';
4+
35
import { noCacheFetchOptions } from '@/lib/cache/http';
46

57
import { rootUrl } from './links';
@@ -73,7 +75,7 @@ export async function getResizedImageURL(
7375
return null;
7476
}
7577

76-
const signature = await generateSignature(input);
78+
const signature = await generateSignatureV1(input);
7779

7880
return (options) => {
7981
const url = new URL('/~gitbook/image', rootUrl());
@@ -93,6 +95,7 @@ export async function getResizedImageURL(
9395
}
9496

9597
url.searchParams.set('sign', signature);
98+
url.searchParams.set('sv', '1');
9699

97100
return url.toString();
98101
};
@@ -101,8 +104,12 @@ export async function getResizedImageURL(
101104
/**
102105
* Verify a signature of an image URL
103106
*/
104-
export async function verifyImageSignature(input: string, signature: string): Promise<boolean> {
105-
const expectedSignature = await generateSignature(input);
107+
export async function verifyImageSignature(
108+
input: string,
109+
{ signature, version }: { signature: string; version: '1' | '0' },
110+
): Promise<boolean> {
111+
const expectedSignature =
112+
version === '1' ? await generateSignatureV1(input) : await generateSignatureV0(input);
106113
return expectedSignature === signature;
107114
}
108115

@@ -201,7 +208,25 @@ function stringifyOptions(options: CloudflareImageOptions): string {
201208
}, '');
202209
}
203210

204-
async function generateSignature(input: string): Promise<string> {
211+
// Reused buffer for FNV-1a hashing
212+
const fnv1aUtf8Buffer = new Uint8Array(512);
213+
214+
/**
215+
* New and faster algorithm to generate a signature for an image.
216+
* When setting it in a URL, we use version '1' for the 'sv' querystring parameneter
217+
* to know that it was the algorithm that was used.
218+
*/
219+
async function generateSignatureV1(input: string): Promise<string> {
220+
const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':');
221+
return fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
222+
}
223+
224+
/**
225+
* Initial algorithm used to generate a signature for an image. It didn't use any versioning in the URL.
226+
* We still need it to validate older signatures that were generated without versioning
227+
* but still exist in previously generated and cached content.
228+
*/
229+
async function generateSignatureV0(input: string): Promise<string> {
205230
const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':');
206231
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(all));
207232

0 commit comments

Comments
 (0)