Skip to content

Commit

Permalink
Merge pull request #1299 from ably/1292-use-web-crypto-for-encrypting…
Browse files Browse the repository at this point in the history
…-and-decrypting

Use Web Crypto API for encrypting and decrypting
  • Loading branch information
lawrence-forooghian authored Jun 6, 2023
2 parents f7dd898 + fcdb9f3 commit 2e0e90c
Show file tree
Hide file tree
Showing 16 changed files with 259 additions and 335 deletions.
6 changes: 3 additions & 3 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,7 @@ declare namespace Types {
*/
interface ChannelOptions {
/**
* Requests encryption for this channel when not null, and specifies encryption-related parameters (such as algorithm, chaining mode, key length and key). See [an example](https://ably.com/docs/realtime/encryption#getting-started).
* Requests encryption for this channel when not null, and specifies encryption-related parameters (such as algorithm, chaining mode, key length and key). See [an example](https://ably.com/docs/realtime/encryption#getting-started). When running in a browser, encryption is only available when the current environment is a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).
*/
cipher?: CipherParamOptions | CipherParams;
/**
Expand Down Expand Up @@ -2904,11 +2904,11 @@ declare namespace Types {
*/
type CipherKeyParam = ArrayBuffer | Uint8Array | string; // if string must be base64-encoded
/**
* Typed differently depending on platform. (`WordArray` in browser, `Buffer` in node)
* Typed differently depending on platform. (`ArrayBuffer` in browser, `Buffer` in node)
*
* @internal
*/
type CipherKey = unknown; // WordArray on browsers, Buffer on node, using unknown as
type CipherKey = unknown; // ArrayBuffer on browsers, Buffer on node, using unknown as
// user should not be interacting with it - output of getDefaultParams should be used opaquely

/**
Expand Down
6 changes: 2 additions & 4 deletions src/common/lib/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,11 @@ class Message {
deltaBase = Platform.BufferUtils.utf8Encode(deltaBase);
}

/* vcdiff expects Uint8Arrays, can't copy with ArrayBuffers. (also, if we
* don't have a TextDecoder, deltaBase might be a WordArray here, so need
* to process it into a buffer anyway) */
// vcdiff expects Uint8Arrays, can't copy with ArrayBuffers.
deltaBase = Platform.BufferUtils.toBuffer(deltaBase as Buffer);
data = Platform.BufferUtils.toBuffer(data);

data = Platform.BufferUtils.typedArrayToBuffer(context.plugins.vcdiff.decode(data, deltaBase));
data = Platform.BufferUtils.arrayBufferViewToBuffer(context.plugins.vcdiff.decode(data, deltaBase));
lastPayload = data;
} catch (e) {
throw new ErrorInfo('Vcdiff delta decode failed with ' + e, 40018, 400);
Expand Down
3 changes: 1 addition & 2 deletions src/common/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import * as NodeBufferUtils from '../platform/nodejs/lib/util/bufferutils';
type Bufferlike = WebBufferUtils.Bufferlike | NodeBufferUtils.Bufferlike;
type BufferUtilsOutput = WebBufferUtils.Output | NodeBufferUtils.Output;
type ToBufferOutput = WebBufferUtils.ToBufferOutput | NodeBufferUtils.ToBufferOutput;
type ComparableBuffer = WebBufferUtils.ComparableBuffer | NodeBufferUtils.ComparableBuffer;
type WordArrayLike = WebBufferUtils.WordArrayLike | NodeBufferUtils.WordArrayLike;

export default class Platform {
Expand All @@ -23,7 +22,7 @@ export default class Platform {
BufferUtils object that accepts a broader range of data types than it
can in reality handle.
*/
static BufferUtils: IBufferUtils<Bufferlike, BufferUtilsOutput, ToBufferOutput, ComparableBuffer, WordArrayLike>;
static BufferUtils: IBufferUtils<Bufferlike, BufferUtilsOutput, ToBufferOutput, WordArrayLike>;
/*
This should be a class whose static methods implement the ICryptoStatic
interface, but (for the same reasons as described in the BufferUtils
Expand Down
11 changes: 4 additions & 7 deletions src/common/types/IBufferUtils.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { TypedArray } from './IPlatformConfig';

export default interface IBufferUtils<Bufferlike, Output, ToBufferOutput, ComparableBuffer, WordArrayLike> {
export default interface IBufferUtils<Bufferlike, Output, ToBufferOutput, WordArrayLike> {
base64CharSet: string;
hexCharSet: string;
isBuffer: (buffer: unknown) => buffer is Bufferlike;
isArrayBuffer: (buffer: unknown) => buffer is ArrayBuffer;
isWordArray: (val: unknown) => val is WordArrayLike;
// On browser this returns a Uint8Array, on node a Buffer
toBuffer: (buffer: Bufferlike) => ToBufferOutput;
toArrayBuffer: (buffer: Bufferlike) => ArrayBuffer;
toArrayBuffer: (buffer: Bufferlike | WordArrayLike) => ArrayBuffer;
base64Encode: (buffer: Bufferlike) => string;
base64Decode: (string: string) => Output;
hexEncode: (buffer: Bufferlike) => string;
hexDecode: (string: string) => Output;
utf8Encode: (string: string) => Output;
utf8Decode: (buffer: Bufferlike) => string;
bufferCompare: (buffer1: ComparableBuffer, buffer2: ComparableBuffer) => number;
areBuffersEqual: (buffer1: Bufferlike, buffer2: Bufferlike) => boolean;
byteLength: (buffer: Bufferlike) => number;
typedArrayToBuffer: (typedArray: TypedArray) => Bufferlike;
arrayBufferViewToBuffer: (arrayBufferView: ArrayBufferView) => Bufferlike;
toWordArray: (buffer: Bufferlike | number[]) => WordArrayLike;
}
17 changes: 3 additions & 14 deletions src/common/types/IPlatformConfig.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
export type TypedArray =
| Int8Array
| Uint8Array
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Uint8ClampedArray
| Float32Array
| Float64Array;

interface MsgPack {
encode(value: any, sparse?: boolean): Buffer | ArrayBuffer | undefined;
decode(buffer: Buffer): any;
Expand All @@ -31,7 +20,7 @@ export interface IPlatformConfig {
stringByteSize: Buffer.byteLength;
addEventListener?: typeof window.addEventListener | typeof global.addEventListener | null;
Promise: typeof Promise;
getRandomValues?: (arr: TypedArray, callback?: (error: Error | null) => void) => void;
getRandomValues?: (arr: ArrayBufferView, callback?: (error: Error | null) => void) => void;
userAgent?: string | null;
inherits?: typeof import('util').inherits;
currentUrl?: string;
Expand All @@ -44,9 +33,9 @@ export interface IPlatformConfig {
atob?: typeof atob | null;
TextEncoder?: typeof TextEncoder;
TextDecoder?: typeof TextDecoder;
getRandomWordArray?: (
getRandomArrayBuffer?: (
byteLength: number,
callback: (err: Error | null, result: CryptoJS.lib.WordArray | null) => void
callback: (err: Error | null, result: ArrayBuffer | null) => void
) => void;
isWebworker?: boolean;
}
6 changes: 0 additions & 6 deletions src/common/types/crypto-js.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ declare module 'crypto-js/build/enc-base64' {
export const stringify: typeof CryptoJS.enc.Base64.stringify;
}

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

declare module 'crypto-js/build/enc-utf8' {
import CryptoJS from 'crypto-js';
export const parse: typeof CryptoJS.enc.Utf8.parse;
Expand Down
13 changes: 9 additions & 4 deletions src/platform/nodejs/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypedArray, IPlatformConfig } from '../../common/types/IPlatformConfig';
import { IPlatformConfig } from '../../common/types/IPlatformConfig';
import crypto from 'crypto';
import WebSocket from 'ws';
import util from 'util';
Expand All @@ -19,9 +19,14 @@ const Config: IPlatformConfig = {
stringByteSize: Buffer.byteLength,
inherits: util.inherits,
addEventListener: null,
getRandomValues: function (arr: TypedArray, callback?: (err: Error | null) => void): void {
const bytes = crypto.randomBytes(arr.length);
arr.set(bytes);
getRandomValues: function (arr: ArrayBufferView, callback?: (err: Error | null) => void): void {
const bytes = crypto.randomBytes(arr.byteLength);
const dataView = new DataView(arr.buffer, arr.byteOffset, arr.byteLength);

for (let i = 0; i < bytes.length; i++) {
dataView.setUint8(i, bytes[i]);
}

if (callback) {
callback(null);
}
Expand Down
32 changes: 14 additions & 18 deletions src/platform/nodejs/lib/util/bufferutils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { TypedArray } from 'common/types/IPlatformConfig';
import IBufferUtils from 'common/types/IBufferUtils';

export type Bufferlike = Buffer | ArrayBuffer | TypedArray;
export type Bufferlike = Buffer | ArrayBuffer | ArrayBufferView;
export type Output = Buffer;
export type ToBufferOutput = Buffer;
export type ComparableBuffer = Buffer;
export type WordArrayLike = never;

class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, ComparableBuffer, WordArrayLike> {
class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, WordArrayLike> {
base64CharSet: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
hexCharSet: string = '0123456789abcdef';

Expand All @@ -19,10 +17,9 @@ class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, Co
return this.toBuffer(buffer).toString('base64');
}

bufferCompare(buffer1: ComparableBuffer, buffer2: ComparableBuffer): number {
if (!buffer1) return -1;
if (!buffer2) return 1;
return buffer1.compare(buffer2);
areBuffersEqual(buffer1: Bufferlike, buffer2: Bufferlike): boolean {
if (!buffer1 || !buffer2) return false;
return this.toBuffer(buffer1).compare(this.toBuffer(buffer2)) == 0;
}

byteLength(buffer: Bufferlike): number {
Expand All @@ -37,17 +34,13 @@ class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, Co
return this.toBuffer(buffer).toString('hex');
}

isArrayBuffer(ob: unknown): ob is ArrayBuffer {
return ob !== null && ob !== undefined && (ob as ArrayBuffer).constructor === ArrayBuffer;
}

/* In node, BufferUtils methods that return binary objects return a Buffer
* for historical reasons; the browser equivalents return ArrayBuffers */
isBuffer(buffer: unknown): buffer is Bufferlike {
return Buffer.isBuffer(buffer) || this.isArrayBuffer(buffer) || ArrayBuffer.isView(buffer);
return Buffer.isBuffer(buffer) || buffer instanceof ArrayBuffer || ArrayBuffer.isView(buffer);
}

toArrayBuffer(buffer: Bufferlike): ArrayBuffer {
toArrayBuffer(buffer: Bufferlike | WordArrayLike): ArrayBuffer {
const nodeBuffer = this.toBuffer(buffer);
return nodeBuffer.buffer.slice(nodeBuffer.byteOffset, nodeBuffer.byteOffset + nodeBuffer.byteLength);
}
Expand All @@ -56,11 +49,14 @@ class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, Co
if (Buffer.isBuffer(buffer)) {
return buffer;
}
return Buffer.from(buffer);
if (buffer instanceof ArrayBuffer) {
return Buffer.from(buffer);
}
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}

typedArrayToBuffer(typedArray: TypedArray): Buffer {
return this.toBuffer(typedArray.buffer);
arrayBufferViewToBuffer(arrayBufferView: ArrayBufferView): Buffer {
return this.toBuffer(arrayBufferView.buffer);
}

utf8Decode(buffer: Bufferlike): string {
Expand All @@ -75,7 +71,7 @@ class BufferUtils implements IBufferUtils<Bufferlike, Output, ToBufferOutput, Co
}

// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
toWordArray(buffer: TypedArray | WordArrayLike | number[] | ArrayBuffer): never {
toWordArray(buffer: ArrayBufferView | WordArrayLike | number[] | ArrayBuffer): never {
throw new Error('Not implemented');
}

Expand Down
8 changes: 4 additions & 4 deletions src/platform/nodejs/lib/util/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ var CryptoFactory = function (bufferUtils: typeof BufferUtils) {
* in any not provided with default values, calculating a keyLength from
* the supplied key, and validating the result.
* @param params an object containing at a minimum a `key` key with value the
* key, as either a binary (ArrayBuffer, Array, WordArray) or a
* base64-encoded string. May optionally also contain: algorithm (defaults to
* AES), mode (defaults to 'cbc')
* key, as either a binary or a base64-encoded string.
* May optionally also contain: algorithm (defaults to AES),
* mode (defaults to 'cbc')
*/
static getDefaultParams(params: API.Types.CipherParamOptions) {
var key: NodeCipherKey;
Expand All @@ -160,7 +160,7 @@ var CryptoFactory = function (bufferUtils: typeof BufferUtils) {

if (typeof params.key === 'string') {
key = bufferUtils.base64Decode(normaliseBase64(params.key));
} else if (bufferUtils.isArrayBuffer(params.key)) {
} else if (params.key instanceof ArrayBuffer) {
key = Buffer.from(params.key);
} else {
key = params.key;
Expand Down
90 changes: 45 additions & 45 deletions src/platform/react-native/config.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
import msgpack from '../web/lib/util/msgpack';
import { parse as parseBase64 } from 'crypto-js/build/enc-base64';
import { IPlatformConfig } from '../../common/types/IPlatformConfig';
import BufferUtils from '../web/lib/util/bufferutils';

const Platform: IPlatformConfig = {
agent: 'reactnative',
logTimestamps: true,
noUpgrade: false,
binaryType: 'arraybuffer',
WebSocket: WebSocket,
xhrSupported: true,
allowComet: true,
streamingSupported: true,
useProtocolHeartbeats: true,
createHmac: null,
msgpack: msgpack,
supportsBinary: !!(typeof TextDecoder !== 'undefined' && TextDecoder),
preferBinary: false,
ArrayBuffer: typeof ArrayBuffer !== 'undefined' && ArrayBuffer,
atob: global.atob,
nextTick: function (f: Function) {
setTimeout(f, 0);
},
addEventListener: null,
inspect: JSON.stringify,
stringByteSize: function (str: string) {
/* str.length will be an underestimate for non-ascii strings. But if we're
* in a browser too old to support TextDecoder, not much we can do. Better
* to underestimate, so if we do go over-size, the server will reject the
* message */
return (typeof TextDecoder !== 'undefined' && new TextEncoder().encode(str).length) || str.length;
},
TextEncoder: global.TextEncoder,
TextDecoder: global.TextDecoder,
Promise: global.Promise,
getRandomWordArray: (function (RNRandomBytes) {
return function (byteLength: number, callback: (err: Error | null, result: CryptoJS.lib.WordArray | null) => void) {
RNRandomBytes.randomBytes(byteLength, function (err: Error | null, base64String: string | null) {
callback(err, base64String ? parseBase64(base64String) : null);
});
};
// Installing @types/react-native would fix this but conflicts with @types/node
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/15960
// eslint-disable-next-line @typescript-eslint/no-var-requires
})(require('react-native').NativeModules.RNRandomBytes),
};

export default Platform;
export default function (bufferUtils: typeof BufferUtils): IPlatformConfig {
return {
agent: 'reactnative',
logTimestamps: true,
noUpgrade: false,
binaryType: 'arraybuffer',
WebSocket: WebSocket,
xhrSupported: true,
allowComet: true,
streamingSupported: true,
useProtocolHeartbeats: true,
createHmac: null,
msgpack: msgpack,
supportsBinary: !!(typeof TextDecoder !== 'undefined' && TextDecoder),
preferBinary: false,
ArrayBuffer: typeof ArrayBuffer !== 'undefined' && ArrayBuffer,
atob: global.atob,
nextTick: function (f: Function) {
setTimeout(f, 0);
},
addEventListener: null,
inspect: JSON.stringify,
stringByteSize: function (str: string) {
/* str.length will be an underestimate for non-ascii strings. But if we're
* in a browser too old to support TextDecoder, not much we can do. Better
* to underestimate, so if we do go over-size, the server will reject the
* message */
return (typeof TextDecoder !== 'undefined' && new TextEncoder().encode(str).length) || str.length;
},
TextEncoder: global.TextEncoder,
TextDecoder: global.TextDecoder,
Promise: global.Promise,
getRandomArrayBuffer: (function (RNRandomBytes) {
return function (byteLength: number, callback: (err: Error | null, result: ArrayBuffer | null) => void) {
RNRandomBytes.randomBytes(byteLength, function (err: Error | null, base64String: string | null) {
callback(err, base64String ? bufferUtils.toArrayBuffer(bufferUtils.base64Decode(base64String)) : null);
});
};
// Installing @types/react-native would fix this but conflicts with @types/node
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/15960
// eslint-disable-next-line @typescript-eslint/no-var-requires
})(require('react-native').NativeModules.RNRandomBytes),
};
}
4 changes: 3 additions & 1 deletion src/platform/react-native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import BufferUtils from '../web/lib/util/bufferutils';
// @ts-ignore
import CryptoFactory from '../web/lib/util/crypto';
import Http from '../web/lib/util/http';
import Config from './config';
import configFactory from './config';
// @ts-ignore
import Transports from '../web/lib/transport';
import Logger from '../../common/lib/util/logger';
Expand All @@ -17,6 +17,8 @@ import WebStorage from '../web/lib/util/webstorage';
import PlatformDefaults from '../web/lib/util/defaults';
import msgpack from '../web/lib/util/msgpack';

const Config = configFactory(BufferUtils);

const Crypto = CryptoFactory(Config, BufferUtils);

Platform.Crypto = Crypto;
Expand Down
4 changes: 2 additions & 2 deletions src/platform/web/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import msgpack from './lib/util/msgpack';
import { IPlatformConfig, TypedArray } from '../../common/types/IPlatformConfig';
import { IPlatformConfig } from '../../common/types/IPlatformConfig';
import * as Utils from 'common/lib/util/utils';

// Workaround for salesforce lightning locker compat
Expand Down Expand Up @@ -65,7 +65,7 @@ const Config: IPlatformConfig = {
if (crypto === undefined) {
return undefined;
}
return function (arr: TypedArray, callback?: (error: Error | null) => void) {
return function (arr: ArrayBufferView, callback?: (error: Error | null) => void) {
crypto.getRandomValues(arr);
if (callback) {
callback(null);
Expand Down
Loading

0 comments on commit 2e0e90c

Please sign in to comment.