diff --git a/keyserver/.eslintrc.json b/keyserver/.eslintrc.json --- a/keyserver/.eslintrc.json +++ b/keyserver/.eslintrc.json @@ -2,5 +2,8 @@ "env": { "node": true, "jest": true + }, + "globals": { + "Blob": false } } 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 @@ -1,12 +1,15 @@ // @flow import type { ResponseFailure } from '@parse/node-apn'; +import crypto from 'crypto'; import type { FirebaseApp, FirebaseError } from 'firebase-admin'; import invariant from 'invariant'; -import fetch from 'node-fetch'; +import nodeFetch from 'node-fetch'; import type { Response } from 'node-fetch'; +import uuid from 'uuid'; import webpush from 'web-push'; +import blobService from 'lib/facts/blob-service.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import type { WebNotification, @@ -14,6 +17,9 @@ } from 'lib/types/notif-types.js'; import { threadSubscriptions } from 'lib/types/subscription-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; +import { toBase64URL } from 'lib/utils/base64.js'; +import { makeBlobServiceEndpointURL } from 'lib/utils/blob-service.js'; +import { getMessageForException } from 'lib/utils/errors.js'; import { getAPNPushProfileForCodeVersion, @@ -28,6 +34,7 @@ TargetedAndroidNotification, } from './types.js'; import { dbQuery, SQL } from '../database/database.js'; +import { generateKey, encrypt } from '../utils/aes-crypto-utils.js'; const fcmTokenInvalidationErrors = new Set([ 'messaging/registration-token-not-registered', @@ -354,7 +361,7 @@ } try { - const result = await fetch(url, { + const result = await nodeFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', @@ -374,8 +381,91 @@ } } +async function blobServiceUpload(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); + + const formData = new FormData(); + const payloadBlob = new Blob([encryptedPayloadBuffer]); + + formData.append('blob_hash', blobHash); + formData.append('blob_data', payloadBlob); + + const assignHolderPromise = fetch( + makeBlobServiceEndpointURL(blobService.httpEndpoints.ASSIGN_HOLDER), + { + method: blobService.httpEndpoints.ASSIGN_HOLDER.method, + body: JSON.stringify({ + holder: blobHolder, + blob_hash: blobHash, + }), + headers: { + 'content-type': 'application/json', + }, + }, + ); + + const uploadHolderPromise = fetch( + makeBlobServiceEndpointURL(blobService.httpEndpoints.UPLOAD_BLOB), + { + method: blobService.httpEndpoints.UPLOAD_BLOB.method, + body: formData, + }, + ); + + try { + const [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: `Notification payload upload failed with HTTP ${status}: ${statusText}`, + }; + } + } catch (e) { + return { + blobUploadError: `Notification payload upload failed with: ${ + getMessageForException(e) ?? 'unknown error' + }`, + }; + } + + const encryptionKeyString = Buffer.from(encryptionKey).toString('base64'); + return { + blobHash, + encryptionKey: encryptionKeyString, + }; +} + export { apnPush, + blobServiceUpload, fcmPush, webPush, wnsPush, diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "prettier": "^2.8.4" }, "resolutions": { - "react-native-flipper": "https://registry.yarnpkg.com/@favware/skip-dependency/-/skip-dependency-1.1.1.tgz" + "react-native-flipper": "https://registry.yarnpkg.com/@favware/skip-dependency/-/skip-dependency-1.1.1.tgz", + "eslint/**/globals": "^13.20.0" } } diff --git a/yarn.lock b/yarn.lock --- a/yarn.lock +++ b/yarn.lock @@ -12679,10 +12679,10 @@ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.10.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676" - integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g== +globals@^13.20.0, globals@^13.6.0, globals@^13.9.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== dependencies: type-fest "^0.20.2"