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

Stop using CryptoJS for signing token requests #1325

Merged
29 changes: 10 additions & 19 deletions src/common/lib/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import Logger from '../util/logger';
import * as Utils from '../util/utils';
import Multicaster from '../util/multicaster';
import ErrorInfo, { IPartialErrorInfo } from '../types/errorinfo';
import HmacSHA256 from 'crypto-js/build/hmac-sha256';
import { stringify as stringifyBase64 } from 'crypto-js/build/enc-base64';
import { parse as parseUtf8 } from 'crypto-js/build/enc-utf8';
import { createHmac } from 'crypto';
import { ErrnoException, RequestCallback, RequestParams } from '../../types/http';
import * as API from '../../../../ably';
import { StandardCallback } from '../../types/utils';
Expand Down Expand Up @@ -44,19 +40,19 @@ function normaliseAuthcallbackError(err: any) {
}

let toBase64 = (str: string): string => {
if (Platform.Config.createHmac) {
return Buffer.from(str, 'ascii').toString('base64');
}
return stringifyBase64(parseUtf8(str));
const buffer = Platform.BufferUtils.utf8Encode(str);
return Platform.BufferUtils.base64Encode(buffer);
};

let hmac = (text: string, key: string): string => {
if (Platform.Config.createHmac) {
const inst = (Platform.Config.createHmac as typeof createHmac)('SHA256', key);
inst.update(text);
return inst.digest('base64');
}
return stringifyBase64(HmacSHA256(text, key));
const bufferUtils = Platform.BufferUtils;

const textBuffer = bufferUtils.utf8Encode(text);
const keyBuffer = bufferUtils.utf8Encode(key);

const digest = bufferUtils.hmacSha256(textBuffer, keyBuffer);

return bufferUtils.base64Encode(digest);
};

function c14n(capability?: string | Record<string, Array<string>>) {
Expand Down Expand Up @@ -134,11 +130,6 @@ class Auth {

if (useTokenAuth(options)) {
/* Token auth */
if (options.key && !hmac) {
const msg = 'client-side token request signing not supported';
Logger.logAction(Logger.LOG_ERROR, 'Auth()', msg);
throw new Error(msg);
}
if (noWayToRenew(options)) {
Logger.logAction(
Logger.LOG_ERROR,
Expand Down
1 change: 1 addition & 0 deletions src/common/types/IBufferUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export default interface IBufferUtils<Bufferlike, Output, ToBufferOutput, WordAr
byteLength: (buffer: Bufferlike) => number;
arrayBufferViewToBuffer: (arrayBufferView: ArrayBufferView) => Bufferlike;
toWordArray: (buffer: Bufferlike | number[]) => WordArrayLike;
hmacSha256(message: Bufferlike, key: Bufferlike): Output;
}
3 changes: 0 additions & 3 deletions src/common/types/IPlatformConfig.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ export interface IPlatformConfig {
binaryType: BinaryType;
WebSocket: typeof WebSocket | typeof import('ws');
useProtocolHeartbeats: boolean;
createHmac:
| ((algorithm: string, key: import('crypto').BinaryLike | import('crypto').KeyObject) => import('crypto').Hmac)
| null;
msgpack: MsgPack;
supportsBinary: boolean;
preferBinary: boolean;
Expand Down
12 changes: 0 additions & 12 deletions src/common/types/crypto-js.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,6 @@ declare module 'crypto-js/build' {
export { algo };
}

declare module 'crypto-js/build/enc-base64' {
import CryptoJS from 'crypto-js';
export const parse: typeof CryptoJS.enc.Base64.parse;
export const stringify: typeof CryptoJS.enc.Base64.stringify;
}

declare module 'crypto-js/build/enc-utf8' {
import CryptoJS from 'crypto-js';
export const parse: typeof CryptoJS.enc.Utf8.parse;
export const stringify: typeof CryptoJS.enc.Utf8.stringify;
}

declare module 'crypto-js/build/lib-typedarrays' {
import CryptoJS from 'crypto-js';
export default CryptoJS.lib.WordArray;
Expand Down
1 change: 0 additions & 1 deletion src/platform/nativescript/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ var Config = {
allowComet: true,
streamingSupported: false,
useProtocolHeartbeats: true,
createHmac: null,
msgpack: msgpack,
supportsBinary: typeof TextDecoder !== 'undefined' && TextDecoder,
preferBinary: false,
Expand Down
1 change: 0 additions & 1 deletion src/platform/nodejs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const Config: IPlatformConfig = {
binaryType: 'nodebuffer' as BinaryType,
WebSocket,
useProtocolHeartbeats: false,
createHmac: crypto.createHmac,
msgpack: require('@ably/msgpack-js'),
supportsBinary: true,
preferBinary: true,
Expand Down
11 changes: 11 additions & 0 deletions src/platform/nodejs/lib/util/bufferutils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import IBufferUtils from 'common/types/IBufferUtils';
import crypto from 'crypto';

export type Bufferlike = Buffer | ArrayBuffer | ArrayBufferView;
export type Output = Buffer;
Expand Down Expand Up @@ -79,6 +80,16 @@ class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, Wo
isWordArray(val: unknown): val is never {
return false;
}

hmacSha256(message: Bufferlike, key: Bufferlike): Output {
const messageBuffer = this.toBuffer(message);
const keyBuffer = this.toBuffer(key);

const hmac = crypto.createHmac('SHA256', keyBuffer);
hmac.update(messageBuffer);

return hmac.digest();
}
}

export default new BufferUtils();
1 change: 0 additions & 1 deletion src/platform/react-native/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default function (bufferUtils: typeof BufferUtils): IPlatformConfig {
allowComet: true,
streamingSupported: true,
useProtocolHeartbeats: true,
createHmac: null,
msgpack: msgpack,
supportsBinary: !!(typeof TextDecoder !== 'undefined' && TextDecoder),
preferBinary: false,
Expand Down
1 change: 0 additions & 1 deletion src/platform/web/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const Config: IPlatformConfig = {
allowComet: allowComet(),
streamingSupported: true,
useProtocolHeartbeats: true,
createHmac: null,
msgpack: msgpack,
supportsBinary: !!globalObject.TextDecoder,
preferBinary: false,
Expand Down
5 changes: 5 additions & 0 deletions src/platform/web/lib/util/bufferutils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import WordArray from 'crypto-js/build/lib-typedarrays';
import Platform from 'common/platform';
import IBufferUtils from 'common/types/IBufferUtils';
import { hmac as hmacSha256 } from './hmac-sha256';

/* Most BufferUtils methods that return a binary object return an ArrayBuffer
* The exception is toBuffer, which returns a Uint8Array (and won't work on
Expand Down Expand Up @@ -215,6 +216,10 @@ class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, Wo
arrayBufferViewToBuffer(arrayBufferView: ArrayBufferView) {
return arrayBufferView.buffer;
}

hmacSha256(message: Bufferlike, key: Bufferlike): Output {
return hmacSha256(this.toBuffer(key), this.toBuffer(message));
}
}

export default new BufferUtils();
217 changes: 217 additions & 0 deletions src/platform/web/lib/util/hmac-sha256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* Copied from https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8
*
* "A simple, open-source, HMAC-SHA256 implementation in pure JavaScript. Designed for efficient minification."
*
* I asked about licensing, and the author said:
*
* > Feel free to use it however you'd like 😄 As the gist title indicates,
* > this is "a simple open source implementation". Feel free to choose whatever
* > license you find most permissible, but I offer no warranty for the code.
* > It's 100% free to do with as you please.
*/

// To ensure cross-browser support even without a proper SubtleCrypto
// impelmentation (or without access to the impelmentation, as is the case with
// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256
// HMAC signatures using nothing but raw JavaScript

/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */

// By giving internal functions names that we can mangle, future calls to
// them are reduced to a single byte (minor space savings in minified file)
var uint8Array = Uint8Array;
var uint32Array = Uint32Array;
var pow = Math.pow;

// Will be initialized below
// Using a Uint32Array instead of a simple array makes the minified code
// a bit bigger (we lose our `unshift()` hack), but comes with huge
// performance gains
var DEFAULT_STATE = new uint32Array(8);
var ROUND_CONSTANTS: number[] = [];

// Reusable object for expanded message
// Using a Uint32Array instead of a simple array makes the minified code
// 7 bytes larger, but comes with huge performance gains
var M = new uint32Array(64);

// After minification the code to compute the default state and round
// constants is smaller than the output. More importantly, this serves as a
// good educational aide for anyone wondering where the magic numbers come
// from. No magic numbers FTW!
function getFractionalBits(n: number) {
return ((n - (n | 0)) * pow(2, 32)) | 0;
}

var n = 2,
nPrime = 0;
while (nPrime < 64) {
// isPrime() was in-lined from its original function form to save
// a few bytes
var isPrime = true;
// Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes
// var sqrtN = pow(n, 1 / 2);
// So technically to determine if a number is prime you only need to
// check numbers up to the square root. However this function only runs
// once and we're only computing the first 64 primes (up to 311), so on
// any modern CPU this whole function runs in a couple milliseconds.
// By going to n / 2 instead of sqrt(n) we net 8 byte savings and no
// scaling performance cost
for (var factor = 2; factor <= n / 2; factor++) {
if (n % factor === 0) {
isPrime = false;
}
}
if (isPrime) {
if (nPrime < 8) {
DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2));
}
ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3));

nPrime++;
}

n++;
}

// For cross-platform support we need to ensure that all 32-bit words are
// in the same endianness. A UTF-8 TextEncoder will return BigEndian data,
// so upon reading or writing to our ArrayBuffer we'll only swap the bytes
// if our system is LittleEndian (which is about 99% of CPUs)
var LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0];

function convertEndian(word: number) {
if (LittleEndian) {
return (
// byte 1 -> byte 4
(word >>> 24) |
// byte 2 -> byte 3
(((word >>> 16) & 0xff) << 8) |
// byte 3 -> byte 2
((word & 0xff00) << 8) |
// byte 4 -> byte 1
(word << 24)
);
} else {
return word;
}
}

function rightRotate(word: number, bits: number) {
return (word >>> bits) | (word << (32 - bits));
}

function sha256(data: Uint8Array) {
// Copy default state
var STATE = DEFAULT_STATE.slice();

// Caching this reduces occurrences of ".length" in minified JavaScript
// 3 more byte savings! :D
var legth = data.length;

// Pad data
var bitLength = legth * 8;
var newBitLength = 512 - ((bitLength + 64) % 512) - 1 + bitLength + 65;

// "bytes" and "words" are stored BigEndian
var bytes = new uint8Array(newBitLength / 8);
var words = new uint32Array(bytes.buffer);

bytes.set(data, 0);
// Append a 1
bytes[legth] = 0b10000000;
// Store length in BigEndian
words[words.length - 1] = convertEndian(bitLength);

// Loop iterator (avoid two instances of "var") -- saves 2 bytes
var round;

// Process blocks (512 bits / 64 bytes / 16 words at a time)
for (var block = 0; block < newBitLength / 32; block += 16) {
var workingState = STATE.slice();

// Rounds
for (round = 0; round < 64; round++) {
var MRound;
// Expand message
if (round < 16) {
// Convert to platform Endianness for later math
MRound = convertEndian(words[block + round]);
} else {
var gamma0x = M[round - 15];
var gamma1x = M[round - 2];
MRound =
M[round - 7] +
M[round - 16] +
(rightRotate(gamma0x, 7) ^ rightRotate(gamma0x, 18) ^ (gamma0x >>> 3)) +
(rightRotate(gamma1x, 17) ^ rightRotate(gamma1x, 19) ^ (gamma1x >>> 10));
}

// M array matches platform endianness
M[round] = MRound |= 0;

// Computation
var t1 =
(rightRotate(workingState[4], 6) ^ rightRotate(workingState[4], 11) ^ rightRotate(workingState[4], 25)) +
((workingState[4] & workingState[5]) ^ (~workingState[4] & workingState[6])) +
workingState[7] +
MRound +
ROUND_CONSTANTS[round];
var t2 =
(rightRotate(workingState[0], 2) ^ rightRotate(workingState[0], 13) ^ rightRotate(workingState[0], 22)) +
((workingState[0] & workingState[1]) ^ (workingState[2] & (workingState[0] ^ workingState[1])));
for (var i = 7; i > 0; i--) {
workingState[i] = workingState[i - 1];
}
workingState[0] = (t1 + t2) | 0;
workingState[4] = (workingState[4] + t1) | 0;
}

// Update state
for (round = 0; round < 8; round++) {
STATE[round] = (STATE[round] + workingState[round]) | 0;
}
}

// Finally the state needs to be converted to BigEndian for output
// And we want to return a Uint8Array, not a Uint32Array
return new uint8Array(
new uint32Array(
STATE.map(function (val) {
return convertEndian(val);
})
).buffer
);
}

export function hmac(key: Uint8Array, data: Uint8Array) {
if (key.length > 64) key = sha256(key);

if (key.length < 64) {
const tmp = new Uint8Array(64);
tmp.set(key, 0);
key = tmp;
}

// Generate inner and outer keys
var innerKey = new Uint8Array(64);
var outerKey = new Uint8Array(64);
for (var i = 0; i < 64; i++) {
innerKey[i] = 0x36 ^ key[i];
outerKey[i] = 0x5c ^ key[i];
}

// Append the innerKey
var msg = new Uint8Array(data.length + 64);
msg.set(innerKey, 0);
msg.set(data, 64);

// Has the previous message and append the outerKey
var result = new Uint8Array(64 + 32);
result.set(outerKey, 0);
result.set(sha256(msg), 64);

// Hash the previous message
return sha256(result);
}