Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix decryption of strings larger than 4 MiB #411

Merged
merged 5 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>
*/
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
}
Loading