From 67c27320882631b890b2743b21a2f826f54f2269 Mon Sep 17 00:00:00 2001 From: Artur Wojciechowski Date: Tue, 21 Jun 2022 15:05:23 +0200 Subject: [PATCH 1/2] fix: refactor base64 encoding to be based on ArrayBuffers --- src/core/components/base64_codec.ts | 55 ++++++++++++++++++++++++++++ src/core/components/token_manager.js | 4 -- src/web/index.js | 36 +----------------- test/unit/base64.test.ts | 28 ++++++++++++++ 4 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 src/core/components/base64_codec.ts create mode 100644 test/unit/base64.test.ts diff --git a/src/core/components/base64_codec.ts b/src/core/components/base64_codec.ts new file mode 100644 index 000000000..04cfea7f7 --- /dev/null +++ b/src/core/components/base64_codec.ts @@ -0,0 +1,55 @@ +const BASE64_CHARMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + +/** + * Decode a Base64 encoded string. + * + * @param paddedInput Base64 string with padding + * @returns ArrayBuffer with decoded data + */ +export function decode(paddedInput: string): ArrayBuffer { + // Remove up to last two equal signs. + const input = paddedInput.replace(/==?$/, ''); + + const outputLength = Math.floor((input.length / 4) * 3); + + // Prepare output buffer. + const data = new ArrayBuffer(outputLength); + const view = new Uint8Array(data); + + let cursor = 0; + + /** + * Returns the next integer representation of a sixtet of bytes from the input + * @returns sixtet of bytes + */ + function nextSixtet() { + const char = input.charAt(cursor++); + const index = BASE64_CHARMAP.indexOf(char); + + if (index === -1) { + throw new Error(`Illegal character at ${cursor}: ${input.charAt(cursor - 1)}`); + } + + return index; + } + + for (let i = 0; i < outputLength; i += 3) { + // Obtain four sixtets + const sx1 = nextSixtet(); + const sx2 = nextSixtet(); + const sx3 = nextSixtet(); + const sx4 = nextSixtet(); + + // Encode them as three octets + const oc1 = ((sx1 & 0b00111111) << 2) | (sx2 >> 4); + const oc2 = ((sx2 & 0b00001111) << 4) | (sx3 >> 2); + const oc3 = ((sx3 & 0b00000011) << 6) | (sx4 >> 0); + + view[i] = oc1; + // Skip padding bytes. + if (sx3 != 64) view[i + 1] = oc2; + if (sx4 != 64) view[i + 2] = oc3; + } + + return data; +} diff --git a/src/core/components/token_manager.js b/src/core/components/token_manager.js index dc8f62611..72dfa8617 100644 --- a/src/core/components/token_manager.js +++ b/src/core/components/token_manager.js @@ -1,7 +1,3 @@ -/* */ -import Config from './config'; -import { GrantTokenOutput } from '../flow_interfaces'; - export default class { _config; diff --git a/src/web/index.js b/src/web/index.js index af55414fb..4d0fff065 100644 --- a/src/web/index.js +++ b/src/web/index.js @@ -4,7 +4,7 @@ import CborReader from 'cbor-js'; import PubNubCore from '../core/pubnub-common'; import Networking from '../networking'; -import CryptoJS from '../core/components/cryptography/hmac-sha256'; +import { decode } from '../core/components/base64_codec'; import Cbor from '../cbor/common'; import { del, get, post, patch, getfile, postfile } from '../networking/modules/web-node'; @@ -19,38 +19,6 @@ function sendBeacon(url) { } } -function base64ToBinary(base64String) { - const parsedWordArray = CryptoJS.enc.Base64.parse(base64String).words; - const arrayBuffer = new ArrayBuffer(parsedWordArray.length * 4); - const view = new Uint8Array(arrayBuffer); - let filledArrayBuffer = null; - let zeroBytesCount = 0; - let byteOffset = 0; - - for (let wordIdx = 0; wordIdx < parsedWordArray.length; wordIdx += 1) { - const word = parsedWordArray[wordIdx]; - byteOffset = wordIdx * 4; - view[byteOffset] = (word & 0xff000000) >> 24; - view[byteOffset + 1] = (word & 0x00ff0000) >> 16; - view[byteOffset + 2] = (word & 0x0000ff00) >> 8; - view[byteOffset + 3] = word & 0x000000ff; - } - - for (let byteIdx = byteOffset + 3; byteIdx >= byteOffset; byteIdx -= 1) { - if (view[byteIdx] === 0 && zeroBytesCount < 3) { - zeroBytesCount += 1; - } - } - - if (zeroBytesCount > 0) { - filledArrayBuffer = view.buffer.slice(0, view.byteLength - zeroBytesCount); - } else { - filledArrayBuffer = view.buffer; - } - - return filledArrayBuffer; -} - function stringifyBufferKeys(obj) { const isObject = (value) => value && typeof value === 'object' && value.constructor === Object; const isString = (value) => typeof value === 'string' || value instanceof String; @@ -99,7 +67,7 @@ export default class extends PubNubCore { getfile, postfile, }); - setup.cbor = new Cbor((arrayBuffer) => stringifyBufferKeys(CborReader.decode(arrayBuffer)), base64ToBinary); + setup.cbor = new Cbor((arrayBuffer) => stringifyBufferKeys(CborReader.decode(arrayBuffer)), decode); setup.PubNubFile = PubNubFile; setup.cryptography = new WebCryptography(); diff --git a/test/unit/base64.test.ts b/test/unit/base64.test.ts new file mode 100644 index 000000000..e3daf4357 --- /dev/null +++ b/test/unit/base64.test.ts @@ -0,0 +1,28 @@ +import { decode } from '../../src/core/components/base64_codec'; + +import assert from 'assert'; + +function assertBufferEqual(actual, expected) { + assert.deepStrictEqual(new Uint8Array(actual), Uint8Array.from(expected)); +} + +describe('base64 codec', () => { + it('should properly handle padding with zero bytes at the end of the data', () => { + const helloWorld = [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + const noZeroBytesResult = decode('SGVsbG8gd29ybGQh'); + const oneZeroBytesResult = decode('SGVsbG8gd29ybGQhAA=='); + const twoZeroBytesResult = decode('SGVsbG8gd29ybGQhAAA='); + const threeZeroBytesResult = decode('SGVsbG8gd29ybGQhAAAA'); + + assertBufferEqual(noZeroBytesResult, helloWorld); + assertBufferEqual(oneZeroBytesResult, [...helloWorld, 0]); + assertBufferEqual(twoZeroBytesResult, [...helloWorld, 0, 0]); + assertBufferEqual(threeZeroBytesResult, [...helloWorld, 0, 0, 0]); + }); + + it('should throw when illegal characters are encountered', () => { + assert.throws(() => { + decode('SGVsbG8g-d29ybGQhAA=='); + }); + }); +}); From 77b12aa7b7e90d98a5ef0b478d267180cd399d50 Mon Sep 17 00:00:00 2001 From: Artur Wojciechowski Date: Tue, 21 Jun 2022 15:26:16 +0200 Subject: [PATCH 2/2] fix: ensure node uses the new Base64 function --- src/node/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/index.ts b/src/node/index.ts index c09c5406a..5d7b3cd73 100755 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -2,6 +2,7 @@ import CborReader from 'cbor-sync'; import PubNubCore from '../core/pubnub-common'; import Networking from '../networking'; import Cbor from '../cbor/common'; +import { decode } from '../core/components/base64_codec'; import { del, get, patch, post, getfile, postfile } from '../networking/modules/web-node'; import { keepAlive, proxy } from '../networking/modules/node'; @@ -10,7 +11,7 @@ import PubNubFile from '../file/modules/node'; export = class extends PubNubCore { constructor(setup: any) { - setup.cbor = new Cbor(CborReader.decode, (base64String: string) => Buffer.from(base64String, 'base64')); + setup.cbor = new Cbor((buffer: ArrayBuffer) => CborReader.decode(Buffer.from(buffer)), decode); setup.networking = new Networking({ keepAlive, del,