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 = '' diff --git a/src/message.ts b/src/message.ts index e89afab..8b4be69 100644 --- a/src/message.ts +++ b/src/message.ts @@ -59,18 +59,72 @@ 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}$/ +/** + * 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-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 { - 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 +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') { @@ -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 }