-
Notifications
You must be signed in to change notification settings - Fork 25
/
core.ts
123 lines (114 loc) · 4.1 KB
/
core.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
120
121
122
123
import { base64 } from 'rfc4648'
import crypto from './crypto'
// @ts-expect-error 2307 - Using esbuild to inline this HTML file as a string
import decryptTemplate from './decrypt-template.html'
/**
* Encrypt a string and turn it into an encrypted payload.
*
* @param {string} content The data to encrypt
* @param {string} password The password used to encrypt + decrypt the content.
* @param {number} iterations The number of iterations to derive the key from the password.
* @returns an encrypted payload
*/
async function getEncryptedPayload(
content: string,
password: string,
iterations: number,
) {
if (iterations < 2e6) {
console.warn(
`[pagecrypt] WARNING: The specified number of password iterations (${iterations}) is not secure. If possible, use at least 2_000_000 or more.`,
)
}
const encoder = new TextEncoder()
const salt = crypto.getRandomValues(new Uint8Array(32))
const baseKey = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey'],
)
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
baseKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt'],
)
const iv = crypto.getRandomValues(new Uint8Array(16))
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(content),
),
)
const totalLength = salt.length + iv.length + ciphertext.length
const mergedData = new Uint8Array(totalLength)
mergedData.set(salt)
mergedData.set(iv, salt.length)
mergedData.set(ciphertext, salt.length + iv.length)
return base64.stringify(mergedData)
}
/**
* Encrypt an HTML string with a given password.
* The resulting page can be viewed and decrypted by opening the output HTML file in a browser, and entering the correct password.
*
* @param {string} inputHTML The HTML string to encrypt.
* @param {string} password The password used to encrypt + decrypt the content.
* @param {number} iterations The number of iterations to derive the key from the password.
* @returns A promise that will resolve with the encrypted HTML content
*/
export async function encryptHTML(
inputHTML: string,
password: string,
iterations: number = 2e6,
) {
return (decryptTemplate as string).replace(
/<!--ENCRYPTED PAYLOAD-->/,
`<pre class="hidden" data-i="${iterations.toExponential()}">${await getEncryptedPayload(
inputHTML,
password,
iterations,
)}</pre>`,
)
}
/**
* Generate a random password of a given length.
*
* @param {number} length The password length.
* @param {string} characters The set of characters to pick from. Max length 255 characters.
* @returns A random password.
*/
export function generatePassword(
length = 80,
characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
) {
if (characters.length > 255) {
throw new Error('[pagecrypt] Max character set length is 255')
}
return Array.from({ length }, (_) => getRandomCharacter(characters)).join(
'',
)
}
/**
* Get a random character from a given set of characters.
*
* @param {string} characters The set of characters to pick from.
* @returns A random character.
*/
function getRandomCharacter(characters: string) {
let randomNumber: number
// Due to the repeating nature of results from the remainder
// operator, we potentially need to regenerate the random number
// several times. This is required to ensure all characters have
// the same probability to get picked. Otherwise, the first
// characters would appear more often, resulting in a weaker
// password security.
// Learn more: https://samuelplumppu.se/blog/generate-password-in-browser-web-crypto-api
do {
randomNumber = crypto.getRandomValues(new Uint8Array(1))[0]
} while (randomNumber >= 256 - (256 % characters.length))
return characters[randomNumber % characters.length]
}