From 8e2b8269a7d94de23d93924f218d9fe769953b7c Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 24 Sep 2024 19:57:16 +0900 Subject: [PATCH 1/5] fix: fix decrypting strings larger than 4 MiB The `cloakedStringRegex` fails to parse ciphertexts larger than about 4 MiB. This is due to [limitations in V8's regex engine][1]. I've adapted the implementation from https://github.com/validatorjs/validator.js/pull/503 under the MIT license, as it solves the error. [1]: https://issues.chromium.org/issues/42207207 --- src/message.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/message.ts b/src/message.ts index e89afab..47e51c6 100644 --- a/src/message.ts +++ b/src/message.ts @@ -62,15 +62,64 @@ export function encryptStringSync( export const cloakedStringRegex = /^v1\.aesgcm256\.(?[0-9a-fA-F]{8})\.(?[a-zA-Z0-9-_]{16})\.(?[a-zA-Z0-9-_]{22,})={0,2}$/ +/** + * Tests if the input string consists only of URL-safe Base64 chars + * (e.g. using `-` and `=` instead of `+` and `/`), and is padded with `=`. + * + * @returns `true` if the string is a valid URL-safe Base64, else `false`. + * + * Adapted from + * @license MIT + * @copyright Copyright (c) 2016 Chris O'Hara + */ +function isBase64(str: string) { + const len = str.length + if (len % 4 === 0 && !/(^[a-zA-Z0-9-_=])/.test(str)) { + return false + } + const firstPaddingChar = str.indexOf('=') + return ( + firstPaddingChar === -1 || + firstPaddingChar === len - 1 || + (firstPaddingChar === len - 2 && str[len - 1] === '=') + ) +} + +function parseCloakedString(input: CloakedString) { + const [version, algorithm, fingerprint, iv, ciphertext, nothing] = + input.split('.') + + const isCloakedString = + version === 'v1' && + algorithm === 'aesgcm256' && + /^[0-9a-fA-F]{8}$/.test(fingerprint) && + /^[a-zA-Z0-9-_]{16}$/.test(iv) && + isBase64(ciphertext) && + ciphertext.length >= 24 && + nothing === undefined + + if (isCloakedString === false) { + return false + } else { + return { + groups: { + fingerprint, + iv, + ciphertext + } + } + } +} + export async function decryptString( input: CloakedString, key: CloakKey | ParsedCloakKey ): Promise { - const match = input.match(cloakedStringRegex) + const match = parseCloakedString(input) if (!match) { throw new Error(`Unknown message format: ${input}`) } - const iv = match.groups!.iv + const iv = match.groups.iv const ciphertext = match.groups!.ciphertext let aesKey: CryptoKey | Uint8Array if (typeof key === 'string') { @@ -88,11 +137,11 @@ export function decryptStringSync( input: CloakedString, key: CloakKey | ParsedCloakKey ): string { - const match = input.match(cloakedStringRegex) + const match = parseCloakedString(input) if (!match) { throw new Error(`Unknown message format: ${input}`) } - const iv = match.groups!.iv + const iv = match.groups.iv const ciphertext = match.groups!.ciphertext let aesKey: CryptoKey | Uint8Array if (typeof key === 'string') { @@ -107,9 +156,9 @@ export function decryptStringSync( } export function getMessageKeyFingerprint(message: CloakedString) { - const match = message.match(cloakedStringRegex) + const match = parseCloakedString(message) if (!match) { throw new Error('Unknown message format') } - return match.groups!.fingerprint + return match.groups.fingerprint } From db7c948c58e7f0dbd15bdecc60f817ea521d8ade Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 24 Sep 2024 19:58:07 +0900 Subject: [PATCH 2/5] feat: export `parseCloakedString` function This function can be used instead of `cloakedStringRegex` on large strings. --- src/message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/message.ts b/src/message.ts index 47e51c6..edf30dc 100644 --- a/src/message.ts +++ b/src/message.ts @@ -85,7 +85,7 @@ function isBase64(str: string) { ) } -function parseCloakedString(input: CloakedString) { +export function parseCloakedString(input: CloakedString) { const [version, algorithm, fingerprint, iv, ciphertext, nothing] = input.split('.') From e19ac07ff02fc64f482531b8d6f4f38c5b2662ce Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 24 Sep 2024 19:59:16 +0900 Subject: [PATCH 3/5] feat: deprecate `cloakedStringRegex` This regex fails on large 4 MiB + strings. `parseCloakedString` should be used instead when possible. --- src/message.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/message.ts b/src/message.ts index edf30dc..e5fe1b9 100644 --- a/src/message.ts +++ b/src/message.ts @@ -59,6 +59,11 @@ export function encryptStringSync( // Decryption -- +/** + * @deprecated + * + * Causes stack errors on large strings, use {@link parseCloakedString} instead. + */ export const cloakedStringRegex = /^v1\.aesgcm256\.(?[0-9a-fA-F]{8})\.(?[a-zA-Z0-9-_]{16})\.(?[a-zA-Z0-9-_]{22,})={0,2}$/ From 52042f33762548431e7f2f5722d6d3758e0bf4ff Mon Sep 17 00:00:00 2001 From: Alois Klink Date: Tue, 24 Sep 2024 19:48:52 +0900 Subject: [PATCH 4/5] test: test decrypting strings larger than 4 MiB This fails in Node.JS v20.17.0 with @47ng/cloak v1.1.0. --- src/index.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 73216fc..ad51dd3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -23,6 +23,14 @@ describe('v1 format', () => { expect(received).toEqual(expected) }) + test('Encrypt / decript 4 MiB string', async () => { + const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' + const expected = 'a'.repeat(4_194_304) // 2 ** 22 = 4 MiB + const cipher = await encryptString(expected, key) + const received = await decryptString(cipher, key) + expect(received).toEqual(expected) + }) + test('Encrypt empty string', async () => { const key = 'k1.aesgcm256.2itF7YmMYIP4b9NNtKMhIx2axGi6aI50RcwGBiFq-VA=' const expected = '' From 70811656171049e8ce296b881191c17d57d663aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Tue, 24 Sep 2024 14:44:54 +0200 Subject: [PATCH 5/5] chore: Make hex & base64 Regexes case-insensitive --- src/message.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/message.ts b/src/message.ts index e5fe1b9..8b4be69 100644 --- a/src/message.ts +++ b/src/message.ts @@ -79,7 +79,7 @@ export const cloakedStringRegex = */ function isBase64(str: string) { const len = str.length - if (len % 4 === 0 && !/(^[a-zA-Z0-9-_=])/.test(str)) { + if (len % 4 === 0 && !/(^[a-z0-9-_=])/i.test(str)) { return false } const firstPaddingChar = str.indexOf('=') @@ -97,7 +97,7 @@ export function parseCloakedString(input: CloakedString) { const isCloakedString = version === 'v1' && algorithm === 'aesgcm256' && - /^[0-9a-fA-F]{8}$/.test(fingerprint) && + /^[0-9a-f]{8}$/i.test(fingerprint) && /^[a-zA-Z0-9-_]{16}$/.test(iv) && isBase64(ciphertext) && ciphertext.length >= 24 &&