Skip to content

Commit

Permalink
feat: Fix decryption of strings larger than 4 MiB (#411)
Browse files Browse the repository at this point in the history
* 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
validatorjs/validator.js#503 under the MIT
license, as it solves the error.

[1]: https://issues.chromium.org/issues/42207207

* feat: export `parseCloakedString` function

This function can be used instead of `cloakedStringRegex` on large
strings.

* feat: deprecate `cloakedStringRegex`

This regex fails on large 4 MiB + strings.
`parseCloakedString` should be used instead when possible.

* test: test decrypting strings larger than 4 MiB

This fails in Node.JS v20.17.0 with @47ng/cloak v1.1.0.

* chore: Make hex & base64 Regexes case-insensitive

---------

Co-authored-by: François Best <github@francoisbest.com>
  • Loading branch information
aloisklink and franky47 authored Sep 24, 2024
1 parent b4eefb7 commit fc619b7
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 6 deletions.
8 changes: 8 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down
66 changes: 60 additions & 6 deletions src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,72 @@ export function encryptStringSync(

// Decryption --

/**
* @deprecated
*
* Causes stack errors on large strings, use {@link parseCloakedString} instead.
*/
export const cloakedStringRegex =
/^v1\.aesgcm256\.(?<fingerprint>[0-9a-fA-F]{8})\.(?<iv>[a-zA-Z0-9-_]{16})\.(?<ciphertext>[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 <https://github.com/validatorjs/validator.js/blob/ebcca98232399b8404ca6b0ec842ab4596329d58/validator.js#L836-L845>
* @license MIT
* @copyright Copyright (c) 2016 Chris O'Hara <cohara87@gmail.com>
*/
function isBase64(str: string) {
const len = str.length
if (len % 4 === 0 && !/(^[a-z0-9-_=])/i.test(str)) {
return false
}
const firstPaddingChar = str.indexOf('=')
return (
firstPaddingChar === -1 ||
firstPaddingChar === len - 1 ||
(firstPaddingChar === len - 2 && str[len - 1] === '=')
)
}

export function parseCloakedString(input: CloakedString) {
const [version, algorithm, fingerprint, iv, ciphertext, nothing] =
input.split('.')

const isCloakedString =
version === 'v1' &&
algorithm === 'aesgcm256' &&
/^[0-9a-f]{8}$/i.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<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') {
Expand All @@ -88,11 +142,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') {
Expand All @@ -107,9 +161,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
}

0 comments on commit fc619b7

Please sign in to comment.