Skip to content

Commit dbfbf3d

Browse files
authored
fix: refactor base64 encoding to be based on ArrayBuffers (#281)
* fix: refactor base64 encoding to be based on ArrayBuffers * fix: ensure node uses the new Base64 function
1 parent f09df2b commit dbfbf3d

File tree

5 files changed

+87
-39
lines changed

5 files changed

+87
-39
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const BASE64_CHARMAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
2+
3+
/**
4+
* Decode a Base64 encoded string.
5+
*
6+
* @param paddedInput Base64 string with padding
7+
* @returns ArrayBuffer with decoded data
8+
*/
9+
export function decode(paddedInput: string): ArrayBuffer {
10+
// Remove up to last two equal signs.
11+
const input = paddedInput.replace(/==?$/, '');
12+
13+
const outputLength = Math.floor((input.length / 4) * 3);
14+
15+
// Prepare output buffer.
16+
const data = new ArrayBuffer(outputLength);
17+
const view = new Uint8Array(data);
18+
19+
let cursor = 0;
20+
21+
/**
22+
* Returns the next integer representation of a sixtet of bytes from the input
23+
* @returns sixtet of bytes
24+
*/
25+
function nextSixtet() {
26+
const char = input.charAt(cursor++);
27+
const index = BASE64_CHARMAP.indexOf(char);
28+
29+
if (index === -1) {
30+
throw new Error(`Illegal character at ${cursor}: ${input.charAt(cursor - 1)}`);
31+
}
32+
33+
return index;
34+
}
35+
36+
for (let i = 0; i < outputLength; i += 3) {
37+
// Obtain four sixtets
38+
const sx1 = nextSixtet();
39+
const sx2 = nextSixtet();
40+
const sx3 = nextSixtet();
41+
const sx4 = nextSixtet();
42+
43+
// Encode them as three octets
44+
const oc1 = ((sx1 & 0b00111111) << 2) | (sx2 >> 4);
45+
const oc2 = ((sx2 & 0b00001111) << 4) | (sx3 >> 2);
46+
const oc3 = ((sx3 & 0b00000011) << 6) | (sx4 >> 0);
47+
48+
view[i] = oc1;
49+
// Skip padding bytes.
50+
if (sx3 != 64) view[i + 1] = oc2;
51+
if (sx4 != 64) view[i + 2] = oc3;
52+
}
53+
54+
return data;
55+
}

src/core/components/token_manager.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
/* */
2-
import Config from './config';
3-
import { GrantTokenOutput } from '../flow_interfaces';
4-
51
export default class {
62
_config;
73

src/node/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import CborReader from 'cbor-sync';
22
import PubNubCore from '../core/pubnub-common';
33
import Networking from '../networking';
44
import Cbor from '../cbor/common';
5+
import { decode } from '../core/components/base64_codec';
56
import { del, get, patch, post, getfile, postfile } from '../networking/modules/web-node';
67
import { keepAlive, proxy } from '../networking/modules/node';
78

@@ -10,7 +11,7 @@ import PubNubFile from '../file/modules/node';
1011

1112
export = class extends PubNubCore {
1213
constructor(setup: any) {
13-
setup.cbor = new Cbor(CborReader.decode, (base64String: string) => Buffer.from(base64String, 'base64'));
14+
setup.cbor = new Cbor((buffer: ArrayBuffer) => CborReader.decode(Buffer.from(buffer)), decode);
1415
setup.networking = new Networking({
1516
keepAlive,
1617
del,

src/web/index.js

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import CborReader from 'cbor-js';
55
import PubNubCore from '../core/pubnub-common';
66
import Networking from '../networking';
7-
import CryptoJS from '../core/components/cryptography/hmac-sha256';
7+
import { decode } from '../core/components/base64_codec';
88
import Cbor from '../cbor/common';
99
import { del, get, post, patch, getfile, postfile } from '../networking/modules/web-node';
1010

@@ -19,38 +19,6 @@ function sendBeacon(url) {
1919
}
2020
}
2121

22-
function base64ToBinary(base64String) {
23-
const parsedWordArray = CryptoJS.enc.Base64.parse(base64String).words;
24-
const arrayBuffer = new ArrayBuffer(parsedWordArray.length * 4);
25-
const view = new Uint8Array(arrayBuffer);
26-
let filledArrayBuffer = null;
27-
let zeroBytesCount = 0;
28-
let byteOffset = 0;
29-
30-
for (let wordIdx = 0; wordIdx < parsedWordArray.length; wordIdx += 1) {
31-
const word = parsedWordArray[wordIdx];
32-
byteOffset = wordIdx * 4;
33-
view[byteOffset] = (word & 0xff000000) >> 24;
34-
view[byteOffset + 1] = (word & 0x00ff0000) >> 16;
35-
view[byteOffset + 2] = (word & 0x0000ff00) >> 8;
36-
view[byteOffset + 3] = word & 0x000000ff;
37-
}
38-
39-
for (let byteIdx = byteOffset + 3; byteIdx >= byteOffset; byteIdx -= 1) {
40-
if (view[byteIdx] === 0 && zeroBytesCount < 3) {
41-
zeroBytesCount += 1;
42-
}
43-
}
44-
45-
if (zeroBytesCount > 0) {
46-
filledArrayBuffer = view.buffer.slice(0, view.byteLength - zeroBytesCount);
47-
} else {
48-
filledArrayBuffer = view.buffer;
49-
}
50-
51-
return filledArrayBuffer;
52-
}
53-
5422
function stringifyBufferKeys(obj) {
5523
const isObject = (value) => value && typeof value === 'object' && value.constructor === Object;
5624
const isString = (value) => typeof value === 'string' || value instanceof String;
@@ -99,7 +67,7 @@ export default class extends PubNubCore {
9967
getfile,
10068
postfile,
10169
});
102-
setup.cbor = new Cbor((arrayBuffer) => stringifyBufferKeys(CborReader.decode(arrayBuffer)), base64ToBinary);
70+
setup.cbor = new Cbor((arrayBuffer) => stringifyBufferKeys(CborReader.decode(arrayBuffer)), decode);
10371

10472
setup.PubNubFile = PubNubFile;
10573
setup.cryptography = new WebCryptography();

test/unit/base64.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { decode } from '../../src/core/components/base64_codec';
2+
3+
import assert from 'assert';
4+
5+
function assertBufferEqual(actual, expected) {
6+
assert.deepStrictEqual(new Uint8Array(actual), Uint8Array.from(expected));
7+
}
8+
9+
describe('base64 codec', () => {
10+
it('should properly handle padding with zero bytes at the end of the data', () => {
11+
const helloWorld = [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33];
12+
const noZeroBytesResult = decode('SGVsbG8gd29ybGQh');
13+
const oneZeroBytesResult = decode('SGVsbG8gd29ybGQhAA==');
14+
const twoZeroBytesResult = decode('SGVsbG8gd29ybGQhAAA=');
15+
const threeZeroBytesResult = decode('SGVsbG8gd29ybGQhAAAA');
16+
17+
assertBufferEqual(noZeroBytesResult, helloWorld);
18+
assertBufferEqual(oneZeroBytesResult, [...helloWorld, 0]);
19+
assertBufferEqual(twoZeroBytesResult, [...helloWorld, 0, 0]);
20+
assertBufferEqual(threeZeroBytesResult, [...helloWorld, 0, 0, 0]);
21+
});
22+
23+
it('should throw when illegal characters are encountered', () => {
24+
assert.throws(() => {
25+
decode('SGVsbG8g-d29ybGQhAA==');
26+
});
27+
});
28+
});

0 commit comments

Comments
 (0)