Page MenuHomePhabricator

D12703.id43081.diff
No OneTemporary

D12703.id43081.diff

diff --git a/lib/facts/identity-service.js b/lib/facts/identity-service.js
--- a/lib/facts/identity-service.js
+++ b/lib/facts/identity-service.js
@@ -2,10 +2,34 @@
import { isDev } from '../utils/dev-utils.js';
-const config: { defaultURL: string } = {
+type IdentityServicePath = '/device_inbound_keys?device_id=';
+
+type IdentityServiceEndpoint = {
+ +path: IdentityServicePath,
+ +method: 'PUT' | 'GET' | 'POST' | 'DELETE',
+};
+
+const httpEndpoints = Object.freeze({
+ GET_INBOUND_KEYS: {
+ path: '/device_inbound_keys?device_id=',
+ method: 'GET',
+ },
+});
+
+type IdentityServiceConfig = {
+ +defaultURL: string,
+ +defaultHttpURL: string,
+ +httpEndpoints: { +[endpoint: string]: IdentityServiceEndpoint },
+};
+
+const config: IdentityServiceConfig = {
defaultURL: isDev
? 'https://identity.staging.commtechnologies.org:50054'
: 'https://identity.commtechnologies.org:50054',
+ defaultHttpURL: isDev
+ ? 'https://identity.staging.commtechnologies.org:51004'
+ : 'https://identity.commtechnologies.org:51004',
+ httpEndpoints,
};
export default config;
diff --git a/lib/utils/identity-service.js b/lib/utils/identity-service.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/identity-service.js
@@ -0,0 +1,48 @@
+// @flow
+
+import type { TInterface } from 'tcomb';
+import t from 'tcomb';
+
+import identityServiceConfig from '../facts/identity-service.js';
+import { tShape } from '../utils/validation-utils.js';
+
+export type InboundKeysForDeviceResponse = {
+ identityKeyInfo: {
+ keyPayload: string,
+ keyPayloadSignature: string,
+ },
+ contentPrekey: {
+ prekey: string,
+ prekeySignature: string,
+ },
+ notifPrekey: {
+ prekey: string,
+ prekeySignature: string,
+ },
+};
+
+export const inboundKeysForDeviceResponseValidator: TInterface<InboundKeysForDeviceResponse> =
+ tShape<InboundKeysForDeviceResponse>({
+ identityKeyInfo: tShape({
+ keyPayload: t.String,
+ keyPayloadSignature: t.String,
+ }),
+ contentPrekey: tShape({
+ prekey: t.String,
+ prekeySignature: t.String,
+ }),
+ notifPrekey: tShape({
+ prekey: t.String,
+ prekeySignature: t.String,
+ }),
+ });
+
+function getInboundKeysForDeviceURL(deviceID: string): string {
+ const urlSafeDeviceID = deviceID.replaceAll('+', '-').replaceAll('/', '_');
+ const endpointBasePath =
+ identityServiceConfig.httpEndpoints.GET_INBOUND_KEYS.path;
+ const path = `${endpointBasePath}${urlSafeDeviceID}`;
+ return `${identityServiceConfig.defaultHttpURL}${path}`;
+}
+
+export { getInboundKeysForDeviceURL };
diff --git a/web/push-notif/push-notifs-handler.js b/web/push-notif/push-notifs-handler.js
--- a/web/push-notif/push-notifs-handler.js
+++ b/web/push-notif/push-notifs-handler.js
@@ -1,5 +1,6 @@
// @flow
+import invariant from 'invariant';
import * as React from 'react';
import { recordAlertActionType } from 'lib/actions/alert-actions.js';
@@ -9,6 +10,7 @@
} from 'lib/actions/device-actions.js';
import { useModalContext } from 'lib/components/modal-provider.react.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
+import { IdentityClientContext } from 'lib/shared/identity-client-context.js';
import { hasMinCodeVersion } from 'lib/shared/version-utils.js';
import {
alertTypes,
@@ -128,19 +130,29 @@
const callSetDeviceToken = useSetDeviceTokenFanout();
const staffCanSee = useStaffCanSee();
+ const identityContext = React.useContext(IdentityClientContext);
+ invariant(identityContext, 'Identity context should be set');
+ const { getAuthMetadata } = identityContext;
+
return React.useCallback(async () => {
if (!publicKey) {
return;
}
const workerRegistration = await navigator.serviceWorker?.ready;
- if (!workerRegistration || !workerRegistration.pushManager) {
+ const authMetadata = await getAuthMetadata();
+ if (
+ !workerRegistration ||
+ !workerRegistration.pushManager ||
+ !authMetadata
+ ) {
return;
}
workerRegistration.active?.postMessage({
olmWasmPath: getOlmWasmPath(),
staffCanSee,
+ authMetadata,
});
const subscription = await workerRegistration.pushManager.subscribe({
@@ -155,7 +167,13 @@
undefined,
{ type: 'device_token', deviceToken: token },
);
- }, [callSetDeviceToken, dispatchActionPromise, publicKey, staffCanSee]);
+ }, [
+ callSetDeviceToken,
+ dispatchActionPromise,
+ publicKey,
+ staffCanSee,
+ getAuthMetadata,
+ ]);
}
function PushNotificationsHandler(): React.Node {
diff --git a/web/push-notif/service-worker.js b/web/push-notif/service-worker.js
--- a/web/push-notif/service-worker.js
+++ b/web/push-notif/service-worker.js
@@ -2,6 +2,7 @@
import localforage from 'localforage';
+import type { AuthMetadata } from 'lib/shared/identity-client-context.js';
import type {
PlainTextWebNotification,
WebNotification,
@@ -15,6 +16,7 @@
type WebNotifsServiceUtilsData,
type WebNotifDecryptionError,
} from './notif-crypto-utils.js';
+import { persistAuthMetadata } from './services-client.js';
import { authoritativeKeyserverID } from '../authoritative-keyserver.js';
import { localforageConfig } from '../shared-worker/utils/constants.js';
@@ -26,7 +28,11 @@
}
declare class CommAppMessage extends ExtendableEvent {
- +data: { +olmWasmPath?: string, +staffCanSee?: boolean };
+ +data: {
+ +olmWasmPath?: string,
+ +staffCanSee?: boolean,
+ +authMetadata?: AuthMetadata,
+ };
}
declare var clients: Clients;
@@ -66,19 +72,24 @@
localforage.config(localforageConfig);
event.waitUntil(
(async () => {
- if (!event.data.olmWasmPath || event.data.staffCanSee === undefined) {
+ const { olmWasmPath, staffCanSee, authMetadata } = event.data;
+
+ if (!olmWasmPath || staffCanSee === undefined || !authMetadata) {
return;
}
const webNotifsServiceUtils: WebNotifsServiceUtilsData = {
- olmWasmPath: event.data.olmWasmPath,
- staffCanSee: event.data.staffCanSee,
+ olmWasmPath: olmWasmPath,
+ staffCanSee: staffCanSee,
};
- await localforage.setItem(
- WEB_NOTIFS_SERVICE_UTILS_KEY,
- webNotifsServiceUtils,
- );
+ await Promise.all([
+ localforage.setItem(
+ WEB_NOTIFS_SERVICE_UTILS_KEY,
+ webNotifsServiceUtils,
+ ),
+ persistAuthMetadata(authMetadata),
+ ]);
await migrateLegacyOlmNotificationsSessions();
})(),
diff --git a/web/push-notif/services-client.js b/web/push-notif/services-client.js
new file mode 100644
--- /dev/null
+++ b/web/push-notif/services-client.js
@@ -0,0 +1,120 @@
+// @flow
+
+import localforage from 'localforage';
+
+import identityServiceConfig from 'lib/facts/identity-service.js';
+import type { AuthMetadata } from 'lib/shared/identity-client-context.js';
+import {
+ identityKeysBlobValidator,
+ type OLMIdentityKeys,
+} from 'lib/types/crypto-types.js';
+import { getMessageForException } from 'lib/utils/errors.js';
+import {
+ getInboundKeysForDeviceURL,
+ inboundKeysForDeviceResponseValidator,
+} from 'lib/utils/identity-service.js';
+import { createHTTPAuthorizationHeader } from 'lib/utils/services-utils.js';
+import { assertWithValidator } from 'lib/utils/validation-utils.js';
+
+import {
+ persistEncryptionKey,
+ retrieveEncryptionKey,
+} from './notif-crypto-utils.js';
+import {
+ type EncryptedData,
+ decryptData,
+ encryptData,
+ generateCryptoKey,
+} from '../crypto/aes-gcm-crypto-utils.js';
+import { isDesktopSafari } from '../shared-worker/utils/db-utils.js';
+
+export const WEB_NOTIFS_SERVICE_CSAT_ENCRYPTION_KEY = 'notifsCSATEncryptionKey';
+export const WEB_NOTIFS_SERVICE_CSAT = 'notifsCSAT';
+
+async function persistAuthMetadata(authMetadata: AuthMetadata): Promise<void> {
+ const encryptionKey = await generateCryptoKey({
+ extractable: isDesktopSafari,
+ });
+
+ const encryptedAuthMetadata = await encryptData(
+ new TextEncoder().encode(JSON.stringify(authMetadata)),
+ encryptionKey,
+ );
+
+ await Promise.all([
+ localforage.setItem(WEB_NOTIFS_SERVICE_CSAT, encryptedAuthMetadata),
+ persistEncryptionKey(WEB_NOTIFS_SERVICE_CSAT_ENCRYPTION_KEY, encryptionKey),
+ ]);
+}
+
+async function fetchAuthMetadata(): Promise<AuthMetadata> {
+ const [encryptionKey, encryptedAuthMetadata] = await Promise.all([
+ retrieveEncryptionKey(WEB_NOTIFS_SERVICE_CSAT_ENCRYPTION_KEY),
+ localforage.getItem<EncryptedData>(WEB_NOTIFS_SERVICE_CSAT),
+ ]);
+
+ if (!encryptionKey || !encryptedAuthMetadata) {
+ throw new Error('CSAT unavailable in push notifs service worker');
+ }
+
+ const authMetadata: AuthMetadata = JSON.parse(
+ new TextDecoder().decode(
+ await decryptData(encryptedAuthMetadata, encryptionKey),
+ ),
+ );
+
+ return authMetadata;
+}
+
+async function getNotifsInboundKeysForDeviceID(
+ deviceID: string,
+ authMetadata: AuthMetadata,
+): Promise<OLMIdentityKeys | { error: string }> {
+ const authorization = createHTTPAuthorizationHeader(authMetadata);
+ const headers = {
+ Authorization: authorization,
+ Accept: 'application/json',
+ };
+ try {
+ const getInboundKeysResponse = await fetch(
+ getInboundKeysForDeviceURL(deviceID),
+ {
+ method: identityServiceConfig.httpEndpoints.GET_INBOUND_KEYS.method,
+ headers,
+ },
+ );
+
+ if (!getInboundKeysResponse.ok) {
+ const { statusText, status } = getInboundKeysResponse;
+ return {
+ error:
+ `Failed to fetch inbound keys for ${deviceID} with code: ${status}. ` +
+ `Details: ${statusText}`,
+ };
+ }
+
+ const inboundKeysForDeviceBlob = await getInboundKeysResponse.json();
+ const inboundKeysForDevice = assertWithValidator(
+ inboundKeysForDeviceBlob,
+ inboundKeysForDeviceResponseValidator,
+ );
+ const identityKeysBlob = inboundKeysForDevice.identityKeyInfo.keyPayload;
+ const identityKeys = assertWithValidator(
+ JSON.parse(identityKeysBlob),
+ identityKeysBlobValidator,
+ );
+ return identityKeys.notificationIdentityPublicKeys;
+ } catch (e) {
+ return {
+ error: `Failed to fetch inbound keys for ${deviceID}. Details: ${
+ getMessageForException(e) ?? ''
+ }`,
+ };
+ }
+}
+
+export {
+ persistAuthMetadata,
+ fetchAuthMetadata,
+ getNotifsInboundKeysForDeviceID,
+};

File Metadata

Mime Type
text/plain
Expires
Fri, Nov 29, 1:02 AM (21 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2595320
Default Alt Text
D12703.id43081.diff (10 KB)

Event Timeline