diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js --- a/keyserver/src/push/crypto.js +++ b/keyserver/src/push/crypto.js @@ -1,6 +1,7 @@ // @flow import apn from '@parse/node-apn'; +import crypto from 'crypto'; import invariant from 'invariant'; import _cloneDeep from 'lodash/fp/cloneDeep.js'; @@ -8,6 +9,7 @@ PlainTextWebNotification, WebNotification, } from 'lib/types/notif-types.js'; +import { toBase64URL } from 'lib/utils/base64.js'; import type { AndroidNotification, @@ -16,6 +18,7 @@ NotificationTargetDevice, } from './types.js'; import { encryptAndUpdateOlmSession } from '../updaters/olm-session-updater.js'; +import { encrypt, generateKey } from '../utils/aes-crypto-utils.js'; import { getOlmUtility } from '../utils/olm-utils.js'; async function encryptIOSNotification( @@ -378,10 +381,37 @@ return Promise.all(notificationPromises); } +async function encryptBlobPayload(payload: string): Promise<{ + +encryptionKey: string, + +encryptedPayload: Blob, + +encryptedPayloadHash: string, +}> { + const encryptionKey = await generateKey(); + const encryptedPayload = await encrypt( + encryptionKey, + new TextEncoder().encode(payload), + ); + const encryptedPayloadBuffer = Buffer.from(encryptedPayload); + const blobHashBase64 = await crypto + .createHash('sha256') + .update(encryptedPayloadBuffer) + .digest('base64'); + const blobHash = toBase64URL(blobHashBase64); + + const payloadBlob = new Blob([encryptedPayloadBuffer]); + const encryptionKeyString = Buffer.from(encryptionKey).toString('base64'); + return { + encryptionKey: encryptionKeyString, + encryptedPayload: payloadBlob, + encryptedPayloadHash: blobHash, + }; +} + export { prepareEncryptedIOSNotifications, prepareEncryptedIOSNotificationRescind, prepareEncryptedAndroidNotifications, prepareEncryptedAndroidNotificationRescinds, prepareEncryptedWebNotifications, + encryptBlobPayload, }; diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js --- a/keyserver/src/push/utils.js +++ b/keyserver/src/push/utils.js @@ -5,12 +5,14 @@ import invariant from 'invariant'; import nodeFetch from 'node-fetch'; import type { Response } from 'node-fetch'; +import uuid from 'uuid'; import webpush from 'web-push'; import type { PlatformDetails } from 'lib/types/device-types.js'; import { threadSubscriptions } from 'lib/types/subscription-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; +import { encryptBlobPayload } from './crypto.js'; import { getAPNPushProfileForCodeVersion, getFCMPushProfileForCodeVersion, @@ -389,7 +391,20 @@ } | { +blobUploadError: string }, > { - return upload(payload); + const blobHolder = uuid.v4(); + try { + const { encryptionKey, encryptedPayload, encryptedPayloadHash } = + await encryptBlobPayload(payload); + await upload(encryptedPayload, encryptedPayloadHash, blobHolder); + return { + blobHash: encryptedPayloadHash, + encryptionKey, + }; + } catch (e) { + return { + blobUploadError: e.message, + }; + } } export { diff --git a/keyserver/src/services/blob.js b/keyserver/src/services/blob.js --- a/keyserver/src/services/blob.js +++ b/keyserver/src/services/blob.js @@ -1,48 +1,21 @@ // @flow -import crypto from 'crypto'; -import uuid from 'uuid'; - import blobService from 'lib/facts/blob-service.js'; -import { toBase64URL } from 'lib/utils/base64.js'; import { makeBlobServiceEndpointURL } from 'lib/utils/blob-service.js'; import { getMessageForException } from 'lib/utils/errors.js'; -import { encrypt, generateKey } from '../utils/aes-crypto-utils.js'; - -async function upload(payload: string): Promise< - | { - +blobHash: string, - +encryptionKey: string, - } - | { +blobUploadError: string }, -> { - const encryptionKey = await generateKey(); - const encryptedPayloadBuffer = Buffer.from( - await encrypt(encryptionKey, new TextEncoder().encode(payload)), - ); - - const blobHolder = uuid.v4(); - const blobHashBase64 = await crypto - .createHash('sha256') - .update(encryptedPayloadBuffer) - .digest('base64'); - - const blobHash = toBase64URL(blobHashBase64); - +async function upload(blob: Blob, hash: string, holder: string): Promise { const formData = new FormData(); - const payloadBlob = new Blob([encryptedPayloadBuffer]); - - formData.append('blob_hash', blobHash); - formData.append('blob_data', payloadBlob); + formData.append('blob_hash', hash); + formData.append('blob_data', blob); const assignHolderPromise = fetch( makeBlobServiceEndpointURL(blobService.httpEndpoints.ASSIGN_HOLDER), { method: blobService.httpEndpoints.ASSIGN_HOLDER.method, body: JSON.stringify({ - holder: blobHolder, - blob_hash: blobHash, + holder, + blob_hash: hash, }), headers: { 'content-type': 'application/json', @@ -58,38 +31,31 @@ }, ); + let assignHolderResponse, uploadBlobResponse; try { - const [assignHolderResponse, uploadBlobResponse] = await Promise.all([ + [assignHolderResponse, uploadBlobResponse] = await Promise.all([ assignHolderPromise, uploadHolderPromise, ]); - - if (!assignHolderResponse.ok) { - const { status, statusText } = assignHolderResponse; - return { - blobUploadError: `Holder assignment failed with HTTP ${status}: ${statusText}`, - }; - } - - if (!uploadBlobResponse.ok) { - const { status, statusText } = uploadBlobResponse; - return { - blobUploadError: `Payload upload failed with HTTP ${status}: ${statusText}`, - }; - } } catch (e) { - return { - blobUploadError: `Payload upload failed with: ${ + throw new Error( + `Payload upload failed with: ${ getMessageForException(e) ?? 'unknown error' }`, - }; + ); } - const encryptionKeyString = Buffer.from(encryptionKey).toString('base64'); - return { - blobHash, - encryptionKey: encryptionKeyString, - }; + if (!assignHolderResponse.ok) { + const { status, statusText } = assignHolderResponse; + throw new Error( + `Holder assignment failed with HTTP ${status}: ${statusText}`, + ); + } + + if (!uploadBlobResponse.ok) { + const { status, statusText } = uploadBlobResponse; + throw new Error(`Payload upload failed with HTTP ${status}: ${statusText}`); + } } export { upload };