-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathsignatures.ts
119 lines (102 loc) · 4.29 KB
/
signatures.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import 'server-only';
import fnv1a from '@sindresorhus/fnv1a';
import type { MaybePromise } from 'p-map';
import { assert } from 'ts-essentials';
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY } from '../env';
/**
* GitBook has supported different version of image signing in the past. To maintain backwards
* compatibility, we retain the ability to verify older signatures.
*/
export type SignatureVersion = '0' | '1' | '2';
/**
* The current version of the signature.
*/
export const CURRENT_SIGNATURE_VERSION: SignatureVersion = '2';
type SignFnInput = {
url: string;
imagesContextId: string;
};
type SignFn = (input: SignFnInput) => MaybePromise<string>;
/**
* Verify a signature of an image URL
*/
export async function verifyImageSignature(
input: SignFnInput,
{ signature, version }: { signature: string; version: SignatureVersion }
): Promise<boolean> {
const generator = IMAGE_SIGNATURE_FUNCTIONS[version];
const generated = await generator(input);
// biome-ignore lint/suspicious/noConsole: we want to log the signature comparison
console.log(
`comparing image signature for "${input.url}" on identifier "${input.imagesContextId}": "${generated}" (expected) === "${signature}" (actual)`
);
return generated === signature;
}
/**
* Generate an image signature. Also returns the version of the image signing algorithm that was used.
*
* This function is sync. If you need to implement an async version of image signing, you'll need to change
* ths signature of this fn and where it's used.
*/
export async function generateImageSignature(input: SignFnInput): Promise<{
signature: string;
version: SignatureVersion;
}> {
const result = await generateSignatureV2(input);
return { signature: result, version: CURRENT_SIGNATURE_VERSION };
}
// Reused buffer for FNV-1a hashing in the v2 algorithm
const fnv1aUtf8Buffer = new Uint8Array(512);
/**
* Generate a signature for an image.
* The signature is relative to the current site being rendered to avoid serving images from other sites on the same domain.
*/
const generateSignatureV2: SignFn = async (input) => {
assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set');
const all = [
input.url,
input.imagesContextId, // The hostname is used to avoid serving images from other sites on the same domain
GITBOOK_IMAGE_RESIZE_SIGNING_KEY,
]
.filter(Boolean)
.join(':');
const signature = fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16);
return signature;
};
// Reused buffer for FNV-1a hashing in the v1 algorithm
const fnv1aUtf8BufferV1 = new Uint8Array(512);
/**
* New and faster algorithm to generate a signature for an image.
* When setting it in a URL, we use version '1' for the 'sv' querystring parameneter
* to know that it was the algorithm that was used.
*/
const generateSignatureV1: SignFn = async (input) => {
assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set');
const all = [input.url, GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':');
return fnv1a(all, { utf8Buffer: fnv1aUtf8BufferV1 }).toString(16);
};
/**
* Initial algorithm used to generate a signature for an image. It didn't use any versioning in the URL.
* We still need it to validate older signatures that were generated without versioning
* but still exist in previously generated and cached content.
*/
const generateSignatureV0: SignFn = async (input) => {
assert(GITBOOK_IMAGE_RESIZE_SIGNING_KEY, 'GITBOOK_IMAGE_RESIZE_SIGNING_KEY is not set');
const all = [input.url, GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':');
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(all));
// Convert ArrayBuffer to hex string
const hashArray = Array.from(new Uint8Array(hash));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
return hashHex;
};
/**
* A mapping of signature versions to signature functions.
*/
const IMAGE_SIGNATURE_FUNCTIONS: Record<SignatureVersion, SignFn> = {
'0': generateSignatureV0,
'1': generateSignatureV1,
'2': generateSignatureV2,
};
export function isSignatureVersion(input: string): input is SignatureVersion {
return Object.keys(IMAGE_SIGNATURE_FUNCTIONS).includes(input);
}