Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3525492
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
27 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js
index 3544896bf..012a32d6f 100644
--- a/keyserver/src/push/crypto.js
+++ b/keyserver/src/push/crypto.js
@@ -1,387 +1,417 @@
// @flow
import apn from '@parse/node-apn';
+import crypto from 'crypto';
import invariant from 'invariant';
import _cloneDeep from 'lodash/fp/cloneDeep.js';
import type {
PlainTextWebNotification,
WebNotification,
} from 'lib/types/notif-types.js';
+import { toBase64URL } from 'lib/utils/base64.js';
import type {
AndroidNotification,
AndroidNotificationPayload,
AndroidNotificationRescind,
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(
cookieID: string,
notification: apn.Notification,
codeVersion?: ?number,
notificationSizeValidator?: apn.Notification => boolean,
): Promise<{
+notification: apn.Notification,
+payloadSizeExceeded: boolean,
+encryptedPayloadHash?: string,
+encryptionOrder?: number,
}> {
invariant(
!notification.collapseId,
'Collapsible notifications encryption currently not implemented',
);
const encryptedNotification = new apn.Notification();
encryptedNotification.id = notification.id;
encryptedNotification.payload.id = notification.id;
encryptedNotification.topic = notification.topic;
encryptedNotification.sound = notification.aps.sound;
encryptedNotification.pushType = 'alert';
encryptedNotification.mutableContent = true;
const { id, ...payloadSansId } = notification.payload;
const unencryptedPayload = {
...payloadSansId,
badge: notification.aps.badge.toString(),
merged: notification.body,
};
try {
const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
let dbPersistCondition;
if (notificationSizeValidator) {
dbPersistCondition = ({ serializedPayload }) => {
const notifCopy = _cloneDeep(encryptedNotification);
notifCopy.payload.encryptedPayload = serializedPayload.body;
return notificationSizeValidator(notifCopy);
};
}
const {
encryptedMessages: { serializedPayload },
dbPersistConditionViolated,
encryptionOrder,
} = await encryptAndUpdateOlmSession(
cookieID,
'notifications',
{
serializedPayload: unencryptedSerializedPayload,
},
dbPersistCondition,
);
encryptedNotification.payload.encryptedPayload = serializedPayload.body;
if (codeVersion && codeVersion >= 254 && codeVersion % 2 === 0) {
encryptedNotification.aps = {
alert: { body: 'ENCRYPTED' },
...encryptedNotification.aps,
};
}
const encryptedPayloadHash = getOlmUtility().sha256(serializedPayload.body);
return {
notification: encryptedNotification,
payloadSizeExceeded: !!dbPersistConditionViolated,
encryptedPayloadHash,
encryptionOrder,
};
} catch (e) {
console.log('Notification encryption failed: ' + e);
encryptedNotification.body = notification.body;
encryptedNotification.threadId = notification.payload.threadID;
invariant(
typeof notification.aps.badge === 'number',
'Unencrypted notification must have badge as a number',
);
encryptedNotification.badge = notification.aps.badge;
encryptedNotification.payload = {
...encryptedNotification.payload,
...notification.payload,
encryptionFailed: 1,
};
return {
notification: encryptedNotification,
payloadSizeExceeded: notificationSizeValidator
? notificationSizeValidator(_cloneDeep(encryptedNotification))
: false,
};
}
}
async function encryptAndroidNotificationPayload<T>(
cookieID: string,
unencryptedPayload: T,
payloadSizeValidator?: (T | { +encryptedPayload: string }) => boolean,
): Promise<{
+resultPayload: T | { +encryptedPayload: string },
+payloadSizeExceeded: boolean,
+encryptionOrder?: number,
}> {
try {
const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
if (!unencryptedSerializedPayload) {
return {
resultPayload: unencryptedPayload,
payloadSizeExceeded: payloadSizeValidator
? payloadSizeValidator(unencryptedPayload)
: false,
};
}
let dbPersistCondition;
if (payloadSizeValidator) {
dbPersistCondition = ({ serializedPayload }) =>
payloadSizeValidator({ encryptedPayload: serializedPayload.body });
}
const {
encryptedMessages: { serializedPayload },
dbPersistConditionViolated,
encryptionOrder,
} = await encryptAndUpdateOlmSession(
cookieID,
'notifications',
{
serializedPayload: unencryptedSerializedPayload,
},
dbPersistCondition,
);
return {
resultPayload: { encryptedPayload: serializedPayload.body },
payloadSizeExceeded: !!dbPersistConditionViolated,
encryptionOrder,
};
} catch (e) {
console.log('Notification encryption failed: ' + e);
const resultPayload = {
encryptionFailed: '1',
...unencryptedPayload,
};
return {
resultPayload,
payloadSizeExceeded: payloadSizeValidator
? payloadSizeValidator(resultPayload)
: false,
};
}
}
async function encryptAndroidNotification(
cookieID: string,
notification: AndroidNotification,
notificationSizeValidator?: AndroidNotification => boolean,
): Promise<{
+notification: AndroidNotification,
+payloadSizeExceeded: boolean,
+encryptionOrder?: number,
}> {
const { id, badgeOnly, ...unencryptedPayload } = notification.data;
let payloadSizeValidator;
if (notificationSizeValidator) {
payloadSizeValidator = (
payload: AndroidNotificationPayload | { +encryptedPayload: string },
) => {
return notificationSizeValidator({ data: { id, badgeOnly, ...payload } });
};
}
const { resultPayload, payloadSizeExceeded, encryptionOrder } =
await encryptAndroidNotificationPayload(
cookieID,
unencryptedPayload,
payloadSizeValidator,
);
return {
notification: {
data: {
id,
badgeOnly,
...resultPayload,
},
},
payloadSizeExceeded,
encryptionOrder,
};
}
async function encryptAndroidNotificationRescind(
cookieID: string,
notification: AndroidNotificationRescind,
): Promise<AndroidNotificationRescind> {
// We don't validate payload size for rescind
// since they are expected to be small and
// never exceed any FCM limit
const { resultPayload } = await encryptAndroidNotificationPayload(
cookieID,
notification.data,
);
return {
data: resultPayload,
};
}
async function encryptWebNotification(
cookieID: string,
notification: PlainTextWebNotification,
): Promise<{ +notification: WebNotification, +encryptionOrder?: number }> {
const { id, ...payloadSansId } = notification;
const unencryptedSerializedPayload = JSON.stringify(payloadSansId);
try {
const {
encryptedMessages: { serializedPayload },
encryptionOrder,
} = await encryptAndUpdateOlmSession(cookieID, 'notifications', {
serializedPayload: unencryptedSerializedPayload,
});
return {
notification: { id, encryptedPayload: serializedPayload.body },
encryptionOrder,
};
} catch (e) {
console.log('Notification encryption failed: ' + e);
return {
notification: {
id,
encryptionFailed: '1',
...payloadSansId,
},
};
}
}
function prepareEncryptedIOSNotifications(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: apn.Notification,
codeVersion?: ?number,
notificationSizeValidator?: apn.Notification => boolean,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: apn.Notification,
+payloadSizeExceeded: boolean,
+encryptedPayloadHash?: string,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ cookieID, deviceToken }) => {
const notif = await encryptIOSNotification(
cookieID,
notification,
codeVersion,
notificationSizeValidator,
);
return { cookieID, deviceToken, ...notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedIOSNotificationRescind(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: apn.Notification,
codeVersion?: ?number,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: apn.Notification,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const { notification: notif } = await encryptIOSNotification(
cookieID,
notification,
codeVersion,
);
return { deviceToken, cookieID, notification: notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedAndroidNotifications(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: AndroidNotification,
notificationSizeValidator?: (notification: AndroidNotification) => boolean,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: AndroidNotification,
+payloadSizeExceeded: boolean,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const notif = await encryptAndroidNotification(
cookieID,
notification,
notificationSizeValidator,
);
return { deviceToken, cookieID, ...notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedAndroidNotificationRescinds(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: AndroidNotificationRescind,
): Promise<
$ReadOnlyArray<{
+cookieID: string,
+deviceToken: string,
+notification: AndroidNotificationRescind,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const notif = await encryptAndroidNotificationRescind(
cookieID,
notification,
);
return { deviceToken, cookieID, notification: notif };
},
);
return Promise.all(notificationPromises);
}
function prepareEncryptedWebNotifications(
devices: $ReadOnlyArray<NotificationTargetDevice>,
notification: PlainTextWebNotification,
): Promise<
$ReadOnlyArray<{
+deviceToken: string,
+notification: WebNotification,
+encryptionOrder?: number,
}>,
> {
const notificationPromises = devices.map(
async ({ deviceToken, cookieID }) => {
const notif = await encryptWebNotification(cookieID, notification);
return { ...notif, deviceToken };
},
);
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
index 0c5ab92b7..16df511c4 100644
--- a/keyserver/src/push/utils.js
+++ b/keyserver/src/push/utils.js
@@ -1,405 +1,420 @@
// @flow
import type { ResponseFailure } from '@parse/node-apn';
import type { FirebaseApp, FirebaseError } from 'firebase-admin';
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,
getAPNProvider,
getFCMProvider,
ensureWebPushInitialized,
getWNSToken,
} from './providers.js';
import type {
TargetedAPNsNotification,
TargetedAndroidNotification,
TargetedWebNotification,
TargetedWNSNotification,
} from './types.js';
import { dbQuery, SQL } from '../database/database.js';
import { upload } from '../services/blob.js';
const fcmTokenInvalidationErrors = new Set([
'messaging/registration-token-not-registered',
'messaging/invalid-registration-token',
]);
const fcmMaxNotificationPayloadByteSize = 4000;
const apnTokenInvalidationErrorCode = 410;
const apnBadRequestErrorCode = 400;
const apnBadTokenErrorString = 'BadDeviceToken';
const apnMaxNotificationPayloadByteSize = 4096;
const webInvalidTokenErrorCodes = [404, 410];
const wnsInvalidTokenErrorCodes = [404, 410];
const wnsMaxNotificationPayloadByteSize = 5000;
type APNPushResult =
| { +success: true }
| {
+errors: $ReadOnlyArray<ResponseFailure>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function apnPush({
targetedNotifications,
platformDetails,
}: {
+targetedNotifications: $ReadOnlyArray<TargetedAPNsNotification>,
+platformDetails: PlatformDetails,
}): Promise<APNPushResult> {
const pushProfile = getAPNPushProfileForCodeVersion(platformDetails);
const apnProvider = await getAPNProvider(pushProfile);
if (!apnProvider && process.env.NODE_ENV === 'development') {
console.log(`no keyserver/secrets/${pushProfile}.json so ignoring notifs`);
return { success: true };
}
invariant(apnProvider, `keyserver/secrets/${pushProfile}.json should exist`);
const results = await Promise.all(
targetedNotifications.map(({ notification, deviceToken }) => {
return apnProvider.send(notification, deviceToken);
}),
);
const mergedResults = { sent: [], failed: [] };
for (const result of results) {
mergedResults.sent.push(...result.sent);
mergedResults.failed.push(...result.failed);
}
const errors = [];
const invalidTokens = [];
for (const error of mergedResults.failed) {
errors.push(error);
/* eslint-disable eqeqeq */
if (
error.status == apnTokenInvalidationErrorCode ||
(error.status == apnBadRequestErrorCode &&
error.response.reason === apnBadTokenErrorString)
) {
invalidTokens.push(error.device);
}
/* eslint-enable eqeqeq */
}
if (invalidTokens.length > 0) {
return { errors, invalidTokens };
} else if (errors.length > 0) {
return { errors };
} else {
return { success: true };
}
}
type FCMPushResult = {
+success?: true,
+fcmIDs?: $ReadOnlyArray<string>,
+errors?: $ReadOnlyArray<FirebaseError>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function fcmPush({
targetedNotifications,
collapseKey,
codeVersion,
}: {
+targetedNotifications: $ReadOnlyArray<TargetedAndroidNotification>,
+codeVersion: ?number,
+collapseKey?: ?string,
}): Promise<FCMPushResult> {
const pushProfile = getFCMPushProfileForCodeVersion(codeVersion);
const fcmProvider = await getFCMProvider(pushProfile);
if (!fcmProvider && process.env.NODE_ENV === 'development') {
console.log(`no keyserver/secrets/${pushProfile}.json so ignoring notifs`);
return { success: true };
}
invariant(fcmProvider, `keyserver/secrets/${pushProfile}.json should exist`);
const options: Object = {
priority: 'high',
};
if (collapseKey) {
options.collapseKey = collapseKey;
}
// firebase-admin is extremely barebones and has a lot of missing or poorly
// thought-out functionality. One of the issues is that if you send a
// multicast messages and one of the device tokens is invalid, the resultant
// won't explain which of the device tokens is invalid. So we're forced to
// avoid the multicast functionality and call it once per deviceToken.
const promises = [];
for (const { notification, deviceToken } of targetedNotifications) {
promises.push(
fcmSinglePush(fcmProvider, notification, deviceToken, options),
);
}
const pushResults = await Promise.all(promises);
const errors = [];
const ids = [];
const invalidTokens = [];
for (let i = 0; i < pushResults.length; i++) {
const pushResult = pushResults[i];
for (const error of pushResult.errors) {
errors.push(error);
if (fcmTokenInvalidationErrors.has(error.errorInfo.code)) {
invalidTokens.push(targetedNotifications[i].deviceToken);
}
}
for (const id of pushResult.fcmIDs) {
ids.push(id);
}
}
const result = {};
if (ids.length > 0) {
result.fcmIDs = ids;
}
if (errors.length > 0) {
result.errors = errors;
} else {
result.success = true;
}
if (invalidTokens.length > 0) {
result.invalidTokens = invalidTokens;
}
return { ...result };
}
async function fcmSinglePush(
provider: FirebaseApp,
notification: Object,
deviceToken: string,
options: Object,
) {
try {
const deliveryResult = await provider
.messaging()
.sendToDevice(deviceToken, notification, options);
const errors = [];
const ids = [];
for (const fcmResult of deliveryResult.results) {
if (fcmResult.error) {
errors.push(fcmResult.error);
} else if (fcmResult.messageId) {
ids.push(fcmResult.messageId);
}
}
return { fcmIDs: ids, errors };
} catch (e) {
return { fcmIDs: [], errors: [e] };
}
}
async function getUnreadCounts(
userIDs: string[],
): Promise<{ [userID: string]: number }> {
const visPermissionExtractString = `$.${threadPermissions.VISIBLE}.value`;
const notificationExtractString = `$.${threadSubscriptions.home}`;
const query = SQL`
SELECT user, COUNT(thread) AS unread_count
FROM memberships
WHERE user IN (${userIDs}) AND last_message > last_read_message
AND role > 0
AND JSON_EXTRACT(permissions, ${visPermissionExtractString})
AND JSON_EXTRACT(subscription, ${notificationExtractString})
GROUP BY user
`;
const [result] = await dbQuery(query);
const usersToUnreadCounts = {};
for (const row of result) {
usersToUnreadCounts[row.user.toString()] = row.unread_count;
}
for (const userID of userIDs) {
if (usersToUnreadCounts[userID] === undefined) {
usersToUnreadCounts[userID] = 0;
}
}
return usersToUnreadCounts;
}
export type WebPushError = {
+statusCode: number,
+headers: { +[string]: string },
+body: string,
};
type WebPushResult = {
+success?: true,
+errors?: $ReadOnlyArray<WebPushError>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function webPush(
targetedNotifications: $ReadOnlyArray<TargetedWebNotification>,
): Promise<WebPushResult> {
await ensureWebPushInitialized();
const pushResults = await Promise.all(
targetedNotifications.map(
async ({ notification, deviceToken: deviceTokenString }) => {
const deviceToken: PushSubscriptionJSON = JSON.parse(deviceTokenString);
const notificationString = JSON.stringify(notification);
try {
await webpush.sendNotification(deviceToken, notificationString);
} catch (error) {
return { error };
}
return {};
},
),
);
const errors = [];
const invalidTokens = [];
const deviceTokens = targetedNotifications.map(
({ deviceToken }) => deviceToken,
);
for (let i = 0; i < pushResults.length; i++) {
const pushResult = pushResults[i];
if (pushResult.error) {
errors.push(pushResult.error);
if (webInvalidTokenErrorCodes.includes(pushResult.error.statusCode)) {
invalidTokens.push(deviceTokens[i]);
}
}
}
const result = {};
if (errors.length > 0) {
result.errors = errors;
} else {
result.success = true;
}
if (invalidTokens.length > 0) {
result.invalidTokens = invalidTokens;
}
return { ...result };
}
export type WNSPushError = any | string | Response;
type WNSPushResult = {
+success?: true,
+wnsIDs?: $ReadOnlyArray<string>,
+errors?: $ReadOnlyArray<WNSPushError>,
+invalidTokens?: $ReadOnlyArray<string>,
};
async function wnsPush(
targetedNotifications: $ReadOnlyArray<TargetedWNSNotification>,
): Promise<WNSPushResult> {
const token = await getWNSToken();
if (!token && process.env.NODE_ENV === 'development') {
console.log(`no keyserver/secrets/wns_config.json so ignoring notifs`);
return { success: true };
}
invariant(token, `keyserver/secrets/wns_config.json should exist`);
const pushResults = targetedNotifications.map(async targetedNotification => {
const notificationString = JSON.stringify(
targetedNotification.notification,
);
try {
return await wnsSinglePush(
token,
notificationString,
targetedNotification.deviceToken,
);
} catch (error) {
return { error };
}
});
const errors = [];
const notifIDs = [];
const invalidTokens = [];
const deviceTokens = targetedNotifications.map(
({ deviceToken }) => deviceToken,
);
for (let i = 0; i < pushResults.length; i++) {
const pushResult = await pushResults[i];
if (pushResult.error) {
errors.push(pushResult.error);
if (
pushResult.error === 'invalidDomain' ||
wnsInvalidTokenErrorCodes.includes(pushResult.error?.status)
) {
invalidTokens.push(deviceTokens[i]);
}
} else {
notifIDs.push(pushResult.wnsID);
}
}
const result = {};
if (notifIDs.length > 0) {
result.wnsIDs = notifIDs;
}
if (errors.length > 0) {
result.errors = errors;
} else {
result.success = true;
}
if (invalidTokens.length > 0) {
result.invalidTokens = invalidTokens;
}
return { ...result };
}
async function wnsSinglePush(token: string, notification: string, url: string) {
const parsedURL = new URL(url);
const domain = parsedURL.hostname.split('.').slice(-3);
if (
domain[0] !== 'notify' ||
domain[1] !== 'windows' ||
domain[2] !== 'com'
) {
return { error: 'invalidDomain' };
}
try {
const result = await nodeFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'X-WNS-Type': 'wns/raw',
'Authorization': `Bearer ${token}`,
},
body: notification,
});
if (!result.ok) {
return { error: result };
}
const wnsID = result.headers.get('X-WNS-MSG-ID');
invariant(wnsID, 'Missing WNS ID');
return { wnsID };
} catch (err) {
return { error: err };
}
}
async function blobServiceUpload(payload: string): Promise<
| {
+blobHash: string,
+encryptionKey: string,
}
| { +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 {
apnPush,
blobServiceUpload,
fcmPush,
webPush,
wnsPush,
getUnreadCounts,
apnMaxNotificationPayloadByteSize,
fcmMaxNotificationPayloadByteSize,
wnsMaxNotificationPayloadByteSize,
};
diff --git a/keyserver/src/services/blob.js b/keyserver/src/services/blob.js
index 976eb0f0f..271a2285d 100644
--- a/keyserver/src/services/blob.js
+++ b/keyserver/src/services/blob.js
@@ -1,95 +1,61 @@
// @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<void> {
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',
},
},
);
const uploadHolderPromise = fetch(
makeBlobServiceEndpointURL(blobService.httpEndpoints.UPLOAD_BLOB),
{
method: blobService.httpEndpoints.UPLOAD_BLOB.method,
body: formData,
},
);
+ 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 };
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Dec 25, 5:16 PM (6 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2695111
Default Alt Text
(27 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment