Skip to content

Commit

Permalink
Merge pull request #1325 from ably/1295-stop-using-CryptoJS-for-signi…
Browse files Browse the repository at this point in the history
…ng-token-requests

Stop using CryptoJS for signing token requests
  • Loading branch information
lawrence-forooghian authored Jun 12, 2023
2 parents 07d8c90 + 9e50d2e commit d2e87fb
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 38 deletions.
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);
}

0 comments on commit d2e87fb

Please sign in to comment.