Move web_api.js and js/modules/crypto.js to TypeScript

This commit is contained in:
Scott Nonnenberg 2020-03-31 13:03:38 -07:00
parent 71436d18e2
commit 9ab54b9b83
29 changed files with 770 additions and 427 deletions

View file

@ -19,7 +19,7 @@ const {
saveMessage, saveMessage,
setAttachmentDownloadJobPending, setAttachmentDownloadJobPending,
} = require('./data'); } = require('./data');
const { stringFromBytes } = require('./crypto'); const { stringFromBytes } = require('../../ts/Crypto');
module.exports = { module.exports = {
start, start,

View file

@ -19,7 +19,7 @@ const pify = require('pify');
const rimraf = require('rimraf'); const rimraf = require('rimraf');
const electronRemote = require('electron').remote; const electronRemote = require('electron').remote;
const crypto = require('./crypto'); const crypto = require('../../ts/Crypto');
const { dialog, BrowserWindow } = electronRemote; const { dialog, BrowserWindow } = electronRemote;

View file

@ -14,7 +14,7 @@ const {
set, set,
} = require('lodash'); } = require('lodash');
const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto'); const { base64ToArrayBuffer, arrayBufferToBase64 } = require('../../ts/Crypto');
const MessageType = require('./types/message'); const MessageType = require('./types/message');
const { createBatcher } = require('../../ts/util/batcher'); const { createBatcher } = require('../../ts/util/batcher');

View file

@ -6,7 +6,7 @@ const nodeUrl = require('url');
const LinkifyIt = require('linkify-it'); const LinkifyIt = require('linkify-it');
const linkify = LinkifyIt(); const linkify = LinkifyIt();
const { concatenateBytes, getViewOfArrayBuffer } = require('./crypto'); const { concatenateBytes, getViewOfArrayBuffer } = require('../../ts/Crypto');
module.exports = { module.exports = {
assembleChunks, assembleChunks,

View file

@ -17,7 +17,7 @@ const {
intsToByteHighAndLow, intsToByteHighAndLow,
splitBytes, splitBytes,
trimBytes, trimBytes,
} = require('../crypto'); } = require('../../../ts/Crypto');
const REVOKED_CERTIFICATES = []; const REVOKED_CERTIFICATES = [];

View file

@ -2,7 +2,7 @@
const { bindActionCreators } = require('redux'); const { bindActionCreators } = require('redux');
const Backbone = require('../../ts/backbone'); const Backbone = require('../../ts/backbone');
const Crypto = require('./crypto'); const Crypto = require('../../ts/Crypto');
const Data = require('./data'); const Data = require('./data');
const Database = require('./database'); const Database = require('./database');
const Emojis = require('./emojis'); const Emojis = require('./emojis');

View file

@ -9,3 +9,5 @@ export function downloadStickerPack(
fromSync?: boolean; fromSync?: boolean;
} }
): Promise<void>; ): Promise<void>;
export function redactPackId(packId: string): string;

View file

@ -33,7 +33,10 @@ const Queue = require('p-queue').default;
const qs = require('qs'); const qs = require('qs');
const { makeLookup } = require('../../ts/util/makeLookup'); const { makeLookup } = require('../../ts/util/makeLookup');
const { base64ToArrayBuffer, deriveStickerPackKey } = require('./crypto'); const {
base64ToArrayBuffer,
deriveStickerPackKey,
} = require('../../ts/Crypto');
const { const {
addStickerPackReference, addStickerPackReference,
createOrUpdateSticker, createOrUpdateSticker,

View file

@ -2,7 +2,10 @@
const { isFunction, isNumber } = require('lodash'); const { isFunction, isNumber } = require('lodash');
const { createLastMessageUpdate } = require('../../../ts/types/Conversation'); const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
const { arrayBufferToBase64, base64ToArrayBuffer } = require('../crypto'); const {
arrayBufferToBase64,
base64ToArrayBuffer,
} = require('../../../ts/Crypto');
async function computeHash(arraybuffer) { async function computeHash(arraybuffer) {
const hash = await crypto.subtle.digest({ name: 'SHA-512' }, arraybuffer); const hash = await crypto.subtle.digest({ name: 'SHA-512' }, arraybuffer);

View file

@ -1321,7 +1321,7 @@ MessageReceiver.prototype.extend({
throw new Error('Failure: Ask sender to update Signal and resend.'); throw new Error('Failure: Ask sender to update Signal and resend.');
} }
const data = await textsecure.crypto.decryptAttachment( const paddedData = await textsecure.crypto.decryptAttachment(
encrypted, encrypted,
window.Signal.Crypto.base64ToArrayBuffer(key), window.Signal.Crypto.base64ToArrayBuffer(key),
window.Signal.Crypto.base64ToArrayBuffer(digest) window.Signal.Crypto.base64ToArrayBuffer(digest)
@ -1329,15 +1329,15 @@ MessageReceiver.prototype.extend({
if (!_.isNumber(size)) { if (!_.isNumber(size)) {
throw new Error( throw new Error(
`downloadAttachment: Size was not provided, actual size was ${data.byteLength}` `downloadAttachment: Size was not provided, actual size was ${paddedData.byteLength}`
); );
} }
const typedArray = window.Signal.Crypto.getFirstBytes(data, size); const data = window.Signal.Crypto.getFirstBytes(paddedData, size);
return { return {
..._.omit(attachment, 'digest', 'key'), ..._.omit(attachment, 'digest', 'key'),
data: window.Signal.Crypto.typedArrayToArrayBuffer(typedArray), data,
}; };
}, },
handleAttachment(attachment) { handleAttachment(attachment) {

View file

@ -63,6 +63,8 @@
"dependencies": { "dependencies": {
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b", "@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b",
"@sindresorhus/is": "0.8.0", "@sindresorhus/is": "0.8.0",
"@types/node-fetch": "2.5.5",
"@types/websocket": "1.0.0",
"array-move": "2.1.0", "array-move": "2.1.0",
"backbone": "1.3.3", "backbone": "1.3.3",
"blob-util": "1.3.0", "blob-util": "1.3.0",

View file

@ -222,7 +222,7 @@ try {
window.nodeSetImmediate = setImmediate; window.nodeSetImmediate = setImmediate;
const { initialize: initializeWebAPI } = require('./js/modules/web_api'); const { initialize: initializeWebAPI } = require('./ts/WebAPI');
window.WebAPI = initializeWebAPI({ window.WebAPI = initializeWebAPI({
url: config.serverUrl, url: config.serverUrl,
@ -308,17 +308,13 @@ try {
function wrapWithPromise(fn) { function wrapWithPromise(fn) {
return (...args) => Promise.resolve(fn(...args)); return (...args) => Promise.resolve(fn(...args));
} }
function typedArrayToArrayBuffer(typedArray) {
const { buffer, byteOffset, byteLength } = typedArray;
return buffer.slice(byteOffset, byteLength + byteOffset);
}
const externalCurve = { const externalCurve = {
generateKeyPair: () => { generateKeyPair: () => {
const { privKey, pubKey } = curve.generateKeyPair(); const { privKey, pubKey } = curve.generateKeyPair();
return { return {
privKey: typedArrayToArrayBuffer(privKey), privKey: window.Signal.Crypto.typedArrayToArrayBuffer(privKey),
pubKey: typedArrayToArrayBuffer(pubKey), pubKey: window.Signal.Crypto.typedArrayToArrayBuffer(pubKey),
}; };
}, },
createKeyPair: incomingKey => { createKeyPair: incomingKey => {
@ -326,8 +322,8 @@ try {
const { privKey, pubKey } = curve.createKeyPair(incomingKeyBuffer); const { privKey, pubKey } = curve.createKeyPair(incomingKeyBuffer);
return { return {
privKey: typedArrayToArrayBuffer(privKey), privKey: window.Signal.Crypto.typedArrayToArrayBuffer(privKey),
pubKey: typedArrayToArrayBuffer(pubKey), pubKey: window.Signal.Crypto.typedArrayToArrayBuffer(pubKey),
}; };
}, },
calculateAgreement: (pubKey, privKey) => { calculateAgreement: (pubKey, privKey) => {
@ -336,7 +332,7 @@ try {
const buffer = curve.calculateAgreement(pubKeyBuffer, privKeyBuffer); const buffer = curve.calculateAgreement(pubKeyBuffer, privKeyBuffer);
return typedArrayToArrayBuffer(buffer); return window.Signal.Crypto.typedArrayToArrayBuffer(buffer);
}, },
verifySignature: (pubKey, message, signature) => { verifySignature: (pubKey, message, signature) => {
const pubKeyBuffer = Buffer.from(pubKey); const pubKeyBuffer = Buffer.from(pubKey);
@ -357,7 +353,7 @@ try {
const buffer = curve.calculateSignature(privKeyBuffer, messageBuffer); const buffer = curve.calculateSignature(privKeyBuffer, messageBuffer);
return typedArrayToArrayBuffer(buffer); return window.Signal.Crypto.typedArrayToArrayBuffer(buffer);
}, },
validatePubKeyFormat: pubKey => { validatePubKeyFormat: pubKey => {
const pubKeyBuffer = Buffer.from(pubKey); const pubKeyBuffer = Buffer.from(pubKey);

View file

@ -6,7 +6,7 @@ const { readFile } = require('fs');
const config = require('url').parse(window.location.toString(), true).query; const config = require('url').parse(window.location.toString(), true).query;
const { noop, uniqBy } = require('lodash'); const { noop, uniqBy } = require('lodash');
const pMap = require('p-map'); const pMap = require('p-map');
const { deriveStickerPackKey } = require('../js/modules/crypto'); const { deriveStickerPackKey } = require('../ts/Crypto');
const { makeGetter } = require('../preload_utils'); const { makeGetter } = require('../preload_utils');
const { dialog } = remote; const { dialog } = remote;
@ -29,7 +29,7 @@ const Signal = require('../js/modules/signal');
window.Signal = Signal.setup({}); window.Signal = Signal.setup({});
const { initialize: initializeWebAPI } = require('../js/modules/web_api'); const { initialize: initializeWebAPI } = require('../ts/WebAPI');
const WebAPI = initializeWebAPI({ const WebAPI = initializeWebAPI({
url: config.serverUrl, url: config.serverUrl,
@ -143,8 +143,7 @@ window.encryptAndUpload = async (
async function encrypt(data, key, iv) { async function encrypt(data, key, iv) {
const { ciphertext } = await window.textsecure.crypto.encryptAttachment( const { ciphertext } = await window.textsecure.crypto.encryptAttachment(
// Convert Node Buffer to ArrayBuffer window.Signal.Crypto.typedArrayToArrayBuffer(data),
window.Signal.Crypto.concatenateBytes(data),
key, key,
iv iv
); );

View file

@ -1,79 +1,48 @@
/* eslint-env browser */ // Yep, we're doing some bitwise stuff in an encryption-related file
/* global dcodeIO, libsignal */ // tslint:disable no-bitwise
/* eslint-disable camelcase, no-bitwise */ // We want some extra variables to make the decrption algorithm easier to understand
// tslint:disable no-unnecessary-local-variable
module.exports = { // Seems that tslint doesn't understand that crypto.subtle.importKey does return a Promise
arrayBufferToBase64, // tslint:disable await-promise
typedArrayToArrayBuffer,
base64ToArrayBuffer,
bytesFromHexString,
bytesFromString,
concatenateBytes,
constantTimeEqual,
decryptAesCtr,
decryptDeviceName,
decryptAttachment,
decryptFile,
decryptSymmetric,
deriveAccessKey,
deriveStickerPackKey,
encryptAesCtr,
encryptDeviceName,
encryptAttachment,
encryptFile,
encryptSymmetric,
fromEncodedBinaryToArrayBuffer,
getAccessKeyVerifier,
getFirstBytes,
getRandomBytes,
getRandomValue,
getViewOfArrayBuffer,
getZeroes,
hexFromBytes,
highBitsToInt,
hmacSha256,
intsToByteHighAndLow,
splitBytes,
stringFromBytes,
trimBytes,
verifyAccessKey,
};
function typedArrayToArrayBuffer(typedArray) { export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer {
const { buffer, byteOffset, byteLength } = typedArray; const { buffer, byteOffset, byteLength } = typedArray;
return buffer.slice(byteOffset, byteLength + byteOffset);
// tslint:disable-next-line no-unnecessary-type-assertion
return buffer.slice(byteOffset, byteLength + byteOffset) as ArrayBuffer;
} }
function arrayBufferToBase64(arrayBuffer) { export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) {
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64'); return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
} }
function base64ToArrayBuffer(base64string) { export function base64ToArrayBuffer(base64string: string) {
return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer(); return window.dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
} }
function fromEncodedBinaryToArrayBuffer(key) { export function fromEncodedBinaryToArrayBuffer(key: string) {
return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer(); return window.dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
} }
function bytesFromString(string) { export function bytesFromString(string: string) {
return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer(); return window.dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
} }
function stringFromBytes(buffer) { export function stringFromBytes(buffer: ArrayBuffer) {
return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8'); return window.dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
} }
function hexFromBytes(buffer) { export function hexFromBytes(buffer: ArrayBuffer) {
return dcodeIO.ByteBuffer.wrap(buffer).toString('hex'); return window.dcodeIO.ByteBuffer.wrap(buffer).toString('hex');
} }
function bytesFromHexString(string) { export function bytesFromHexString(string: string) {
return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer(); return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
} }
async function deriveStickerPackKey(packKey) { export async function deriveStickerPackKey(packKey: ArrayBuffer) {
const salt = getZeroes(32); const salt = getZeroes(32);
const info = bytesFromString('Sticker Pack'); const info = bytesFromString('Sticker Pack');
const [part1, part2] = await libsignal.HKDF.deriveSecrets( const [part1, part2] = await window.libsignal.HKDF.deriveSecrets(
packKey, packKey,
salt, salt,
info info
@ -84,10 +53,13 @@ async function deriveStickerPackKey(packKey) {
// High-level Operations // High-level Operations
async function encryptDeviceName(deviceName, identityPublic) { export async function encryptDeviceName(
deviceName: string,
identityPublic: ArrayBuffer
) {
const plaintext = bytesFromString(deviceName); const plaintext = bytesFromString(deviceName);
const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair(); const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
const masterSecret = await libsignal.Curve.async.calculateAgreement( const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
identityPublic, identityPublic,
ephemeralKeyPair.privKey ephemeralKeyPair.privKey
); );
@ -108,11 +80,19 @@ async function encryptDeviceName(deviceName, identityPublic) {
}; };
} }
async function decryptDeviceName( export async function decryptDeviceName(
{ ephemeralPublic, syntheticIv, ciphertext } = {}, {
identityPrivate ephemeralPublic,
syntheticIv,
ciphertext,
}: {
ephemeralPublic: ArrayBuffer;
syntheticIv: ArrayBuffer;
ciphertext: ArrayBuffer;
},
identityPrivate: ArrayBuffer
) { ) {
const masterSecret = await libsignal.Curve.async.calculateAgreement( const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
ephemeralPublic, ephemeralPublic,
identityPrivate identityPrivate
); );
@ -134,38 +114,58 @@ async function decryptDeviceName(
} }
// Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa' // Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'
function getAttachmentLabel(path) { export function getAttachmentLabel(path: string) {
const filename = path.slice(3); const filename = path.slice(3);
return base64ToArrayBuffer(filename); return base64ToArrayBuffer(filename);
} }
const PUB_KEY_LENGTH = 32; const PUB_KEY_LENGTH = 32;
async function encryptAttachment(staticPublicKey, path, plaintext) { export async function encryptAttachment(
staticPublicKey: ArrayBuffer,
path: string,
plaintext: ArrayBuffer
) {
const uniqueId = getAttachmentLabel(path); const uniqueId = getAttachmentLabel(path);
return encryptFile(staticPublicKey, uniqueId, plaintext); return encryptFile(staticPublicKey, uniqueId, plaintext);
} }
async function decryptAttachment(staticPrivateKey, path, data) { export async function decryptAttachment(
staticPrivateKey: ArrayBuffer,
path: string,
data: ArrayBuffer
) {
const uniqueId = getAttachmentLabel(path); const uniqueId = getAttachmentLabel(path);
return decryptFile(staticPrivateKey, uniqueId, data); return decryptFile(staticPrivateKey, uniqueId, data);
} }
async function encryptFile(staticPublicKey, uniqueId, plaintext) { export async function encryptFile(
const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair(); staticPublicKey: ArrayBuffer,
const agreement = await libsignal.Curve.async.calculateAgreement( uniqueId: ArrayBuffer,
plaintext: ArrayBuffer
) {
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
const agreement = await window.libsignal.Curve.async.calculateAgreement(
staticPublicKey, staticPublicKey,
ephemeralKeyPair.privKey ephemeralKeyPair.privKey
); );
const key = await hmacSha256(agreement, uniqueId); const key = await hmacSha256(agreement, uniqueId);
const prefix = ephemeralKeyPair.pubKey.slice(1); const prefix = ephemeralKeyPair.pubKey.slice(1);
return concatenateBytes(prefix, await encryptSymmetric(key, plaintext)); return concatenateBytes(prefix, await encryptSymmetric(key, plaintext));
} }
async function decryptFile(staticPrivateKey, uniqueId, data) { export async function decryptFile(
staticPrivateKey: ArrayBuffer,
uniqueId: ArrayBuffer,
data: ArrayBuffer
) {
const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH); const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH);
const ciphertext = _getBytes(data, PUB_KEY_LENGTH, data.byteLength); const ciphertext = _getBytes(data, PUB_KEY_LENGTH, data.byteLength);
const agreement = await libsignal.Curve.async.calculateAgreement( const agreement = await window.libsignal.Curve.async.calculateAgreement(
ephemeralPublicKey, ephemeralPublicKey,
staticPrivateKey staticPrivateKey
); );
@ -175,21 +175,24 @@ async function decryptFile(staticPrivateKey, uniqueId, data) {
return decryptSymmetric(key, ciphertext); return decryptSymmetric(key, ciphertext);
} }
async function deriveAccessKey(profileKey) { export async function deriveAccessKey(profileKey: ArrayBuffer) {
const iv = getZeroes(12); const iv = getZeroes(12);
const plaintext = getZeroes(16); const plaintext = getZeroes(16);
const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext); const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext);
return getFirstBytes(accessKey, 16); return getFirstBytes(accessKey, 16);
} }
async function getAccessKeyVerifier(accessKey) { export async function getAccessKeyVerifier(accessKey: ArrayBuffer) {
const plaintext = getZeroes(32); const plaintext = getZeroes(32);
const hmac = await hmacSha256(accessKey, plaintext);
return hmac; return hmacSha256(accessKey, plaintext);
} }
async function verifyAccessKey(accessKey, theirVerifier) { export async function verifyAccessKey(
accessKey: ArrayBuffer,
theirVerifier: ArrayBuffer
) {
const ourVerifier = await getAccessKeyVerifier(accessKey); const ourVerifier = await getAccessKeyVerifier(accessKey);
if (constantTimeEqual(ourVerifier, theirVerifier)) { if (constantTimeEqual(ourVerifier, theirVerifier)) {
@ -203,7 +206,10 @@ const IV_LENGTH = 16;
const MAC_LENGTH = 16; const MAC_LENGTH = 16;
const NONCE_LENGTH = 16; const NONCE_LENGTH = 16;
async function encryptSymmetric(key, plaintext) { export async function encryptSymmetric(
key: ArrayBuffer,
plaintext: ArrayBuffer
) {
const iv = getZeroes(IV_LENGTH); const iv = getZeroes(IV_LENGTH);
const nonce = getRandomBytes(NONCE_LENGTH); const nonce = getRandomBytes(NONCE_LENGTH);
@ -220,7 +226,7 @@ async function encryptSymmetric(key, plaintext) {
return concatenateBytes(nonce, cipherText, mac); return concatenateBytes(nonce, cipherText, mac);
} }
async function decryptSymmetric(key, data) { export async function decryptSymmetric(key: ArrayBuffer, data: ArrayBuffer) {
const iv = getZeroes(IV_LENGTH); const iv = getZeroes(IV_LENGTH);
const nonce = getFirstBytes(data, NONCE_LENGTH); const nonce = getFirstBytes(data, NONCE_LENGTH);
@ -247,23 +253,25 @@ async function decryptSymmetric(key, data) {
return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText); return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText);
} }
function constantTimeEqual(left, right) { export function constantTimeEqual(left: ArrayBuffer, right: ArrayBuffer) {
if (left.byteLength !== right.byteLength) { if (left.byteLength !== right.byteLength) {
return false; return false;
} }
let result = 0; let result = 0;
const ta1 = new Uint8Array(left); const ta1 = new Uint8Array(left);
const ta2 = new Uint8Array(right); const ta2 = new Uint8Array(right);
for (let i = 0, max = left.byteLength; i < max; i += 1) { const max = left.byteLength;
for (let i = 0; i < max; i += 1) {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
result |= ta1[i] ^ ta2[i]; result |= ta1[i] ^ ta2[i];
} }
return result === 0; return result === 0;
} }
// Encryption // Encryption
async function hmacSha256(key, plaintext) { export async function hmacSha256(key: ArrayBuffer, plaintext: ArrayBuffer) {
const algorithm = { const algorithm = {
name: 'HMAC', name: 'HMAC',
hash: 'SHA-256', hash: 'SHA-256',
@ -273,7 +281,7 @@ async function hmacSha256(key, plaintext) {
const cryptoKey = await window.crypto.subtle.importKey( const cryptoKey = await window.crypto.subtle.importKey(
'raw', 'raw',
key, key,
algorithm, algorithm as any,
extractable, extractable,
['sign'] ['sign']
); );
@ -281,7 +289,11 @@ async function hmacSha256(key, plaintext) {
return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext); return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext);
} }
async function _encrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) { export async function _encrypt_aes256_CBC_PKCSPadding(
key: ArrayBuffer,
iv: ArrayBuffer,
plaintext: ArrayBuffer
) {
const algorithm = { const algorithm = {
name: 'AES-CBC', name: 'AES-CBC',
iv, iv,
@ -291,7 +303,7 @@ async function _encrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
const cryptoKey = await window.crypto.subtle.importKey( const cryptoKey = await window.crypto.subtle.importKey(
'raw', 'raw',
key, key,
algorithm, algorithm as any,
extractable, extractable,
['encrypt'] ['encrypt']
); );
@ -299,7 +311,11 @@ async function _encrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext); return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
} }
async function _decrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) { export async function _decrypt_aes256_CBC_PKCSPadding(
key: ArrayBuffer,
iv: ArrayBuffer,
plaintext: ArrayBuffer
) {
const algorithm = { const algorithm = {
name: 'AES-CBC', name: 'AES-CBC',
iv, iv,
@ -309,14 +325,19 @@ async function _decrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
const cryptoKey = await window.crypto.subtle.importKey( const cryptoKey = await window.crypto.subtle.importKey(
'raw', 'raw',
key, key,
algorithm, algorithm as any,
extractable, extractable,
['decrypt'] ['decrypt']
); );
return window.crypto.subtle.decrypt(algorithm, cryptoKey, plaintext); return window.crypto.subtle.decrypt(algorithm, cryptoKey, plaintext);
} }
async function encryptAesCtr(key, plaintext, counter) { export async function encryptAesCtr(
key: ArrayBuffer,
plaintext: ArrayBuffer,
counter: ArrayBuffer
) {
const extractable = false; const extractable = false;
const algorithm = { const algorithm = {
name: 'AES-CTR', name: 'AES-CTR',
@ -327,7 +348,7 @@ async function encryptAesCtr(key, plaintext, counter) {
const cryptoKey = await crypto.subtle.importKey( const cryptoKey = await crypto.subtle.importKey(
'raw', 'raw',
key, key,
algorithm, algorithm as any,
extractable, extractable,
['encrypt'] ['encrypt']
); );
@ -341,7 +362,11 @@ async function encryptAesCtr(key, plaintext, counter) {
return ciphertext; return ciphertext;
} }
async function decryptAesCtr(key, ciphertext, counter) { export async function decryptAesCtr(
key: ArrayBuffer,
ciphertext: ArrayBuffer,
counter: ArrayBuffer
) {
const extractable = false; const extractable = false;
const algorithm = { const algorithm = {
name: 'AES-CTR', name: 'AES-CTR',
@ -352,7 +377,7 @@ async function decryptAesCtr(key, ciphertext, counter) {
const cryptoKey = await crypto.subtle.importKey( const cryptoKey = await crypto.subtle.importKey(
'raw', 'raw',
key, key,
algorithm, algorithm as any,
extractable, extractable,
['decrypt'] ['decrypt']
); );
@ -361,10 +386,15 @@ async function decryptAesCtr(key, ciphertext, counter) {
cryptoKey, cryptoKey,
ciphertext ciphertext
); );
return plaintext; return plaintext;
} }
async function _encrypt_aes_gcm(key, iv, plaintext) { export async function _encrypt_aes_gcm(
key: ArrayBuffer,
iv: ArrayBuffer,
plaintext: ArrayBuffer
) {
const algorithm = { const algorithm = {
name: 'AES-GCM', name: 'AES-GCM',
iv, iv,
@ -374,32 +404,35 @@ async function _encrypt_aes_gcm(key, iv, plaintext) {
const cryptoKey = await crypto.subtle.importKey( const cryptoKey = await crypto.subtle.importKey(
'raw', 'raw',
key, key,
algorithm, algorithm as any,
extractable, extractable,
['encrypt'] ['encrypt']
); );
return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext); return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
} }
// Utility // Utility
function getRandomBytes(n) { export function getRandomBytes(n: number) {
const bytes = new Uint8Array(n); const bytes = new Uint8Array(n);
window.crypto.getRandomValues(bytes); window.crypto.getRandomValues(bytes);
return bytes;
return typedArrayToArrayBuffer(bytes);
} }
function getRandomValue(low, high) { export function getRandomValue(low: number, high: number): number {
const diff = high - low; const diff = high - low;
const bytes = new Uint32Array(1); const bytes = new Uint32Array(1);
window.crypto.getRandomValues(bytes); window.crypto.getRandomValues(bytes);
// Because high and low are inclusive // Because high and low are inclusive
const mod = diff + 1; const mod = diff + 1;
return (bytes[0] % mod) + low; return (bytes[0] % mod) + low;
} }
function getZeroes(n) { export function getZeroes(n: number) {
const result = new Uint8Array(n); const result = new Uint8Array(n);
const value = 0; const value = 0;
@ -407,28 +440,36 @@ function getZeroes(n) {
const endExclusive = n; const endExclusive = n;
result.fill(value, startIndex, endExclusive); result.fill(value, startIndex, endExclusive);
return result; return typedArrayToArrayBuffer(result);
} }
function highBitsToInt(byte) { export function highBitsToInt(byte: number): number {
return (byte & 0xff) >> 4; return (byte & 0xff) >> 4;
} }
function intsToByteHighAndLow(highValue, lowValue) { export function intsToByteHighAndLow(
highValue: number,
lowValue: number
): number {
return ((highValue << 4) | lowValue) & 0xff; return ((highValue << 4) | lowValue) & 0xff;
} }
function trimBytes(buffer, length) { export function trimBytes(buffer: ArrayBuffer, length: number) {
return getFirstBytes(buffer, length); return getFirstBytes(buffer, length);
} }
function getViewOfArrayBuffer(buffer, start, finish) { export function getViewOfArrayBuffer(
buffer: ArrayBuffer,
start: number,
finish: number
) {
const source = new Uint8Array(buffer); const source = new Uint8Array(buffer);
const result = source.slice(start, finish); const result = source.slice(start, finish);
return result.buffer; return result.buffer;
} }
function concatenateBytes(...elements) { export function concatenateBytes(...elements: Array<ArrayBuffer | Uint8Array>) {
const length = elements.reduce( const length = elements.reduce(
(total, element) => total + element.byteLength, (total, element) => total + element.byteLength,
0 0
@ -437,7 +478,8 @@ function concatenateBytes(...elements) {
const result = new Uint8Array(length); const result = new Uint8Array(length);
let position = 0; let position = 0;
for (let i = 0, max = elements.length; i < max; i += 1) { const max = elements.length;
for (let i = 0; i < max; i += 1) {
const element = new Uint8Array(elements[i]); const element = new Uint8Array(elements[i]);
result.set(element, position); result.set(element, position);
position += element.byteLength; position += element.byteLength;
@ -446,10 +488,13 @@ function concatenateBytes(...elements) {
throw new Error('problem concatenating!'); throw new Error('problem concatenating!');
} }
return result.buffer; return typedArrayToArrayBuffer(result);
} }
function splitBytes(buffer, ...lengths) { export function splitBytes(
buffer: ArrayBuffer,
...lengths: Array<number>
): Array<ArrayBuffer> {
const total = lengths.reduce((acc, length) => acc + length, 0); const total = lengths.reduce((acc, length) => acc + length, 0);
if (total !== buffer.byteLength) { if (total !== buffer.byteLength) {
@ -462,27 +507,34 @@ function splitBytes(buffer, ...lengths) {
const results = []; const results = [];
let position = 0; let position = 0;
for (let i = 0, max = lengths.length; i < max; i += 1) { const max = lengths.length;
for (let i = 0; i < max; i += 1) {
const length = lengths[i]; const length = lengths[i];
const result = new Uint8Array(length); const result = new Uint8Array(length);
const section = source.slice(position, position + length); const section = source.slice(position, position + length);
result.set(section); result.set(section);
position += result.byteLength; position += result.byteLength;
results.push(result); results.push(typedArrayToArrayBuffer(result));
} }
return results; return results;
} }
function getFirstBytes(data, n) { export function getFirstBytes(data: ArrayBuffer, n: number) {
const source = new Uint8Array(data); const source = new Uint8Array(data);
return source.subarray(0, n);
return typedArrayToArrayBuffer(source.subarray(0, n));
} }
// Internal-only // Internal-only
function _getBytes(data, start, n) { export function _getBytes(
data: ArrayBuffer | Uint8Array,
start: number,
n: number
) {
const source = new Uint8Array(data); const source = new Uint8Array(data);
return source.subarray(start, start + n);
return typedArrayToArrayBuffer(source.subarray(start, start + n));
} }

File diff suppressed because it is too large Load diff

7
ts/proxy-agent.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
declare module 'proxy-agent' {
import { Agent } from 'http';
export default class ProxyAgent extends Agent {
constructor(url: string);
}
}

View file

@ -11,17 +11,8 @@ type NetworkActions = {
const REFRESH_INTERVAL = 5000; const REFRESH_INTERVAL = 5000;
interface ShimmedWindow extends Window {
log: {
info: (...args: any) => void;
};
}
const unknownWindow = window as unknown;
const shimmedWindow = unknownWindow as ShimmedWindow;
export function initializeNetworkObserver(networkActions: NetworkActions) { export function initializeNetworkObserver(networkActions: NetworkActions) {
const { log } = shimmedWindow; const { log } = window;
log.info(`Initializing network observer every ${REFRESH_INTERVAL}ms`); log.info(`Initializing network observer every ${REFRESH_INTERVAL}ms`);
const refresh = () => { const refresh = () => {

View file

@ -1,4 +1,3 @@
export function trigger(name: string, param1?: any, param2?: any) { export function trigger(name: string, param1?: any, param2?: any) {
// @ts-ignore
window.Whisper.events.trigger(name, param1, param2); window.Whisper.events.trigger(name, param1, param2);
} }

View file

@ -1,12 +1,5 @@
interface ShimmedWindow extends Window {
getSocketStatus: () => number;
}
const unknownWindow = window as unknown;
const shimmedWindow = unknownWindow as ShimmedWindow;
export function getSocketStatus() { export function getSocketStatus() {
const { getSocketStatus: getMessageReceiverStatus } = shimmedWindow; const { getSocketStatus: getMessageReceiverStatus } = window;
return getMessageReceiverStatus(); return getMessageReceiverStatus();
} }

View file

@ -1,9 +1,7 @@
export async function put(key: string, value: any) { export function put(key: string, value: any) {
// @ts-ignore window.storage.put(key, value);
return window.storage.put(key, value);
} }
export async function remove(key: string) { export function remove(key: string) {
// @ts-ignore window.storage.remove(key);
return window.storage.remove(key);
} }

View file

@ -1,52 +1,9 @@
type LoggerType = (...args: Array<any>) => void;
type TextSecureType = {
storage: {
user: {
getNumber: () => string;
};
get: (item: string) => any;
};
messaging: {
sendStickerPackSync: (
operations: Array<{
packId: string;
packKey: string;
installed: boolean;
}>,
options: Object
) => Promise<void>;
};
};
type ConversationControllerType = {
prepareForSend: (
id: string,
options: Object
) => {
wrap: (promise: Promise<any>) => Promise<void>;
sendOptions: Object;
};
};
interface ShimmedWindow extends Window {
log: {
error: LoggerType;
info: LoggerType;
};
textsecure: TextSecureType;
ConversationController: ConversationControllerType;
}
const unknownWindow = window as unknown;
const shimmedWindow = unknownWindow as ShimmedWindow;
export function sendStickerPackSync( export function sendStickerPackSync(
packId: string, packId: string,
packKey: string, packKey: string,
installed: boolean installed: boolean
) { ) {
const { ConversationController, textsecure, log } = shimmedWindow; const { ConversationController, textsecure, log } = window;
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = ConversationController.prepareForSend(
ourNumber, ourNumber,

View file

@ -11,7 +11,7 @@ export type ItemsStateType = {
type ItemPutAction = { type ItemPutAction = {
type: 'items/PUT'; type: 'items/PUT';
payload: Promise<void>; payload: null;
}; };
type ItemPutExternalAction = { type ItemPutExternalAction = {
@ -24,7 +24,7 @@ type ItemPutExternalAction = {
type ItemRemoveAction = { type ItemRemoveAction = {
type: 'items/REMOVE'; type: 'items/REMOVE';
payload: Promise<void>; payload: null;
}; };
type ItemRemoveExternalAction = { type ItemRemoveExternalAction = {
@ -54,9 +54,11 @@ export const actions = {
}; };
function putItem(key: string, value: any): ItemPutAction { function putItem(key: string, value: any): ItemPutAction {
storageShim.put(key, value);
return { return {
type: 'items/PUT', type: 'items/PUT',
payload: storageShim.put(key, value), payload: null,
}; };
} }
@ -71,9 +73,11 @@ function putItemExternal(key: string, value: any): ItemPutExternalAction {
} }
function removeItem(key: string): ItemRemoveAction { function removeItem(key: string): ItemRemoveAction {
storageShim.remove(key);
return { return {
type: 'items/REMOVE', type: 'items/REMOVE',
payload: storageShim.remove(key), payload: null,
}; };
} }

View file

@ -1,21 +1,9 @@
interface ShimmedWindow extends Window {
getExpiration: () => string;
log: {
info: (...args: any) => void;
error: (...args: any) => void;
};
}
const unknownWindow = window as unknown;
const shimmedWindow = unknownWindow as ShimmedWindow;
// @ts-ignore
const env = window.getEnvironment(); const env = window.getEnvironment();
const NINETY_ONE_DAYS = 86400 * 91 * 1000; const NINETY_ONE_DAYS = 86400 * 91 * 1000;
export function hasExpired() { export function hasExpired() {
const { getExpiration, log } = shimmedWindow; const { getExpiration, log } = window;
let buildExpiration = 0; let buildExpiration = 0;

View file

@ -255,7 +255,7 @@
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/modules/stickers.js", "path": "js/modules/stickers.js",
"line": "async function load() {", "line": "async function load() {",
"lineNumber": 74, "lineNumber": 77,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-04-26T17:48:30.675Z" "updated": "2019-04-26T17:48:30.675Z"
}, },
@ -11613,7 +11613,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/shims/textsecure.js", "path": "ts/shims/textsecure.js",
"line": " wrap(textsecure.messaging.sendStickerPackSync([", "line": " wrap(textsecure.messaging.sendStickerPackSync([",
"lineNumber": 13, "lineNumber": 11,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
}, },
@ -11621,7 +11621,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/shims/textsecure.ts", "path": "ts/shims/textsecure.ts",
"line": " wrap(", "line": " wrap(",
"lineNumber": 64, "lineNumber": 21,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
} }

View file

@ -54,9 +54,10 @@ const excludedFiles = [
// High-traffic files in our project // High-traffic files in our project
'^js/models/messages.js', '^js/models/messages.js',
'^js/modules/crypto.js',
'^js/views/conversation_view.js', '^js/views/conversation_view.js',
'^js/background.js', '^js/background.js',
'^ts/Crypto.js',
'^ts/Crypto.ts',
// Generated files // Generated files
'^js/components.js', '^js/components.js',

View file

@ -5,23 +5,19 @@ export function markEverDone() {
export function markDone() { export function markDone() {
markEverDone(); markEverDone();
// @ts-ignore
window.storage.put('chromiumRegistrationDone', ''); window.storage.put('chromiumRegistrationDone', '');
} }
export function remove() { export function remove() {
// @ts-ignore
window.storage.remove('chromiumRegistrationDone'); window.storage.remove('chromiumRegistrationDone');
} }
export function isDone() { export function isDone() {
// @ts-ignore
// tslint:disable-next-line no-backbone-get-set-outside-model // tslint:disable-next-line no-backbone-get-set-outside-model
return window.storage.get('chromiumRegistrationDone') === ''; return window.storage.get('chromiumRegistrationDone') === '';
} }
export function everDone() { export function everDone() {
// @ts-ignore
// tslint:disable-next-line no-backbone-get-set-outside-model // tslint:disable-next-line no-backbone-get-set-outside-model
return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone(); return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone();
} }

98
ts/window.d.ts vendored Normal file
View file

@ -0,0 +1,98 @@
// Captures the globals put in place by preload.js, background.js and others
declare global {
interface Window {
dcodeIO: DCodeIOType;
getExpiration: () => string;
getEnvironment: () => string;
getSocketStatus: () => number;
libsignal: LibSignalType;
log: {
info: LoggerType;
warn: LoggerType;
error: LoggerType;
};
storage: {
put: (key: string, value: any) => void;
remove: (key: string) => void;
get: (key: string) => any;
};
textsecure: TextSecureType;
ConversationController: ConversationControllerType;
Whisper: WhisperType;
}
}
export type ConversationControllerType = {
prepareForSend: (
id: string,
options: Object
) => {
wrap: (promise: Promise<any>) => Promise<void>;
sendOptions: Object;
};
};
export type DCodeIOType = {
ByteBuffer: {
wrap: (
value: any,
type?: string
) => {
toString: (type: string) => string;
toArrayBuffer: () => ArrayBuffer;
};
};
};
export type LibSignalType = {
KeyHelper: {
generateIdentityKeyPair: () => Promise<{
privKey: ArrayBuffer;
pubKey: ArrayBuffer;
}>;
};
Curve: {
async: {
calculateAgreement: (
publicKey: ArrayBuffer,
privateKey: ArrayBuffer
) => Promise<ArrayBuffer>;
};
};
HKDF: {
deriveSecrets: (
packKey: ArrayBuffer,
salt: ArrayBuffer,
info: ArrayBuffer
) => Promise<Array<ArrayBuffer>>;
};
};
export type LoggerType = (...args: Array<any>) => void;
export type TextSecureType = {
storage: {
user: {
getNumber: () => string;
};
get: (key: string) => any;
};
messaging: {
sendStickerPackSync: (
operations: Array<{
packId: string;
packKey: string;
installed: boolean;
}>,
options: Object
) => Promise<void>;
};
};
export type WhisperType = {
events: {
trigger: (name: string, param1: any, param2: any) => void;
};
};

View file

@ -7,6 +7,7 @@
"align": false, "align": false,
"newline-per-chained-call": false, "newline-per-chained-call": false,
"array-type": [true, "generic"], "array-type": [true, "generic"],
"number-literal-format": false,
// Preferred by Prettier: // Preferred by Prettier:
"arrow-parens": [true, "ban-single-arg-parens"], "arrow-parens": [true, "ban-single-arg-parens"],

View file

@ -2066,6 +2066,14 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.0.0.tgz#a3014921991066193f6c8e47290d4d598dfd19e6" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.0.0.tgz#a3014921991066193f6c8e47290d4d598dfd19e6"
integrity sha512-ZS0vBV7Jn5Z/Q4T3VXauEKMDCV8nWOtJJg90OsDylkYJiQwcWtKuLzohWzrthBkerUF7DLMmJcwOPEP0i/AOXw== integrity sha512-ZS0vBV7Jn5Z/Q4T3VXauEKMDCV8nWOtJJg90OsDylkYJiQwcWtKuLzohWzrthBkerUF7DLMmJcwOPEP0i/AOXw==
"@types/node-fetch@2.5.5":
version "2.5.5"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.5.tgz#cd264e20a81f4600a6c52864d38e7fef72485e92"
integrity sha512-IWwjsyYjGw+em3xTvWVQi5MgYKbRs0du57klfTaZkv/B24AEQ/p/IopNeqIYNy3EsfHOpg8ieQSDomPcsYMHpA==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*": "@types/node@*":
version "11.12.2" version "11.12.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.2.tgz#d7f302e74b10e9801d52852137f652d9ee235da8" resolved "https://registry.yarnpkg.com/@types/node/-/node-11.12.2.tgz#d7f302e74b10e9801d52852137f652d9ee235da8"
@ -2317,6 +2325,13 @@
"@types/webpack-sources" "*" "@types/webpack-sources" "*"
source-map "^0.6.0" source-map "^0.6.0"
"@types/websocket@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.0.tgz#828c794b0a50949ad061aa311af1009934197e4b"
integrity sha512-MLr8hDM8y7vvdAdnoDEP5LotRoYJj7wgT6mWzCUQH/gHqzS4qcnOT/K4dhC0WimWIUiA3Arj9QAJGGKNRiRZKA==
dependencies:
"@types/node" "*"
"@types/yargs-parser@*": "@types/yargs-parser@*":
version "15.0.0" version "15.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@ -4757,6 +4772,13 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^1.0.0: comma-separated-tokens@^1.0.0:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz#419cd7fb3258b1ed838dc0953167a25e152f5b59" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz#419cd7fb3258b1ed838dc0953167a25e152f5b59"
@ -7380,6 +7402,15 @@ form-data@2.3.2, form-data@~2.3.2:
combined-stream "1.0.6" combined-stream "1.0.6"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@~2.1.1: form-data@~2.1.1:
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"