From 919259c960609fc597c268263c0ed3201fe16c98 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Tue, 23 Mar 2021 17:50:02 -0700 Subject: [PATCH] Use non-subtle crypto in libsignal-protocol --- libtextsecure/libsignal-protocol.js | 15 ++---- preload.js | 1 + ts/Crypto.ts | 10 ++-- ts/test-both/util/synchronousCrypto_test.ts | 48 +++++++++++++++++ ts/util/synchronousCrypto.ts | 57 +++++++++++++++++++++ ts/window.d.ts | 2 + 6 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 ts/test-both/util/synchronousCrypto_test.ts create mode 100644 ts/util/synchronousCrypto.ts diff --git a/libtextsecure/libsignal-protocol.js b/libtextsecure/libsignal-protocol.js index b7e8fc14f..1fe5fd733 100644 --- a/libtextsecure/libsignal-protocol.js +++ b/libtextsecure/libsignal-protocol.js @@ -23897,24 +23897,19 @@ var Internal = Internal || {}; crypto.getRandomValues(array); return array.buffer; }, + encrypt: function(key, data, iv) { - return crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) { - return crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); - }); + return Promise.resolve(window.synchronousCrypto.encrypt(key, data, iv)); }, decrypt: function(key, data, iv) { - return crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['decrypt']).then(function(key) { - return crypto.subtle.decrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); - }); + return Promise.resolve(window.synchronousCrypto.decrypt(key, data, iv)); }, sign: function(key, data) { - return crypto.subtle.importKey('raw', key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) { - return crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, data); - }); + return Promise.resolve(window.synchronousCrypto.sign(key, data)); }, hash: function(data) { - return crypto.subtle.digest({name: 'SHA-512'}, data); + return Promise.resolve(window.synchronousCrypto.hash(data)); }, HKDF: function(input, salt, info) { diff --git a/preload.js b/preload.js index fc3888b03..04b794762 100644 --- a/preload.js +++ b/preload.js @@ -411,6 +411,7 @@ try { window.nodeSetImmediate = setImmediate; window.textsecure = require('./ts/textsecure').default; + window.synchronousCrypto = require('./ts/util/synchronousCrypto'); window.WebAPI = window.textsecure.WebAPI.initialize({ url: config.serverUrl, diff --git a/ts/Crypto.ts b/ts/Crypto.ts index db689a7c1..dfb4e1564 100644 --- a/ts/Crypto.ts +++ b/ts/Crypto.ts @@ -5,9 +5,13 @@ import pProps from 'p-props'; import { chunk } from 'lodash'; export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer { - const { buffer, byteOffset, byteLength } = typedArray; - - return buffer.slice(byteOffset, byteLength + byteOffset) as typeof typedArray; + const ab = new ArrayBuffer(typedArray.length); + // Create a new Uint8Array backed by the ArrayBuffer and copy all values from + // the `typedArray` into it by calling `.set()` method. Note that raw + // ArrayBuffer doesn't offer this API, because it is supposed to be used with + // concrete data view (i.e. Uint8Array, Float64Array, and so on.) + new Uint8Array(ab).set(typedArray, 0); + return ab; } export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string { diff --git a/ts/test-both/util/synchronousCrypto_test.ts b/ts/test-both/util/synchronousCrypto_test.ts new file mode 100644 index 000000000..8ad7dae75 --- /dev/null +++ b/ts/test-both/util/synchronousCrypto_test.ts @@ -0,0 +1,48 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import crypto from 'crypto'; + +import { typedArrayToArrayBuffer as toArrayBuffer } from '../../Crypto'; +import { hash, sign, encrypt, decrypt } from '../../util/synchronousCrypto'; + +describe('synchronousCrypto', () => { + describe('hash', () => { + it('returns SHA512 hash of the input', () => { + const result = hash(toArrayBuffer(Buffer.from('signal'))); + assert.strictEqual( + Buffer.from(result).toString('base64'), + 'WxneQjrfSlY95Bi+SAzDAr2cf3mxUXePeNYn6DILN4a8NFr9VelTbP5tGHdthi+' + + 'mrJLqMZd1I6w8CxCnmJ/OFw==' + ); + }); + }); + + describe('sign', () => { + it('returns hmac SHA256 hash of the input', () => { + const result = sign( + toArrayBuffer(Buffer.from('secret')), + toArrayBuffer(Buffer.from('signal')) + ); + + assert.strictEqual( + Buffer.from(result).toString('base64'), + '5ewbITW27c1F7dluF9KwGcVQSxmZp6mpVhPj3ww1Sh8=' + ); + }); + }); + + describe('encrypt+decrypt', () => { + it('returns original input', () => { + const iv = crypto.randomBytes(16); + const key = crypto.randomBytes(32); + const input = Buffer.from('plaintext'); + + const ciphertext = encrypt(key, input, iv); + const plaintext = decrypt(key, ciphertext, iv); + + assert.strictEqual(Buffer.from(plaintext).toString(), 'plaintext'); + }); + }); +}); diff --git a/ts/util/synchronousCrypto.ts b/ts/util/synchronousCrypto.ts new file mode 100644 index 000000000..3d70a2c0c --- /dev/null +++ b/ts/util/synchronousCrypto.ts @@ -0,0 +1,57 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import crypto from 'crypto'; + +import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; + +export function sign(key: ArrayBuffer, data: ArrayBuffer): ArrayBuffer { + return toArrayBuffer( + crypto + .createHmac('sha256', Buffer.from(key)) + .update(Buffer.from(data)) + .digest() + ); +} + +export function hash(data: ArrayBuffer): ArrayBuffer { + return toArrayBuffer( + crypto.createHash('sha512').update(Buffer.from(data)).digest() + ); +} + +export function encrypt( + key: ArrayBuffer, + data: ArrayBuffer, + iv: ArrayBuffer +): ArrayBuffer { + const cipher = crypto.createCipheriv( + 'aes-256-cbc', + Buffer.from(key), + Buffer.from(iv) + ); + const encrypted = Buffer.concat([ + cipher.update(Buffer.from(data)), + cipher.final(), + ]); + + return toArrayBuffer(encrypted); +} + +export function decrypt( + key: ArrayBuffer, + data: ArrayBuffer, + iv: ArrayBuffer +): ArrayBuffer { + const cipher = crypto.createDecipheriv( + 'aes-256-cbc', + Buffer.from(key), + Buffer.from(iv) + ); + const decrypted = Buffer.concat([ + cipher.update(Buffer.from(data)), + cipher.final(), + ]); + + return toArrayBuffer(decrypted); +} diff --git a/ts/window.d.ts b/ts/window.d.ts index 096942f19..98299fdcb 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -95,6 +95,7 @@ import { MIMEType } from './types/MIME'; import { ElectronLocaleType } from './util/mapToSupportLocale'; import { SignalProtocolStore } from './LibSignalStore'; import { StartupQueue } from './util/StartupQueue'; +import * as synchronousCrypto from './util/synchronousCrypto'; export { Long } from 'long'; @@ -247,6 +248,7 @@ declare global { }; systemTheme: WhatIsThis; textsecure: TextSecureType; + synchronousCrypto: typeof synchronousCrypto; titleBarDoubleClick: () => void; unregisterForActive: (handler: () => void) => void; updateTrayIcon: (count: number) => void;