diff --git a/keyserver/src/push/encrypted-notif-utils-api.js b/keyserver/src/push/encrypted-notif-utils-api.js index 9235e881c..ebecee59f 100644 --- a/keyserver/src/push/encrypted-notif-utils-api.js +++ b/keyserver/src/push/encrypted-notif-utils-api.js @@ -1,59 +1,74 @@ // @flow import type { EncryptResult } from '@commapp/olm'; import type { EncryptedNotifUtilsAPI } from 'lib/types/notif-types.js'; import { blobServiceUpload } from './utils.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'; const encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI = { encryptSerializedNotifPayload: async ( cryptoID: string, unencryptedPayload: string, encryptedPayloadSizeValidator?: ( encryptedPayload: string, type: '1' | '0', ) => boolean, ) => { let dbPersistCondition; if (encryptedPayloadSizeValidator) { dbPersistCondition = ({ serializedPayload, }: { +[string]: EncryptResult, }) => encryptedPayloadSizeValidator( serializedPayload.body, serializedPayload.type ? '1' : '0', ); } const { encryptedMessages: { serializedPayload }, dbPersistConditionViolated, encryptionOrder, } = await encryptAndUpdateOlmSession( cryptoID, 'notifications', { serializedPayload: unencryptedPayload, }, dbPersistCondition, ); return { encryptedData: serializedPayload, sizeLimitViolated: dbPersistConditionViolated, encryptionOrder, }; }, uploadLargeNotifPayload: blobServiceUpload, getNotifByteSize: (serializedPayload: string) => Buffer.byteLength(serializedPayload), getEncryptedNotifHash: async (serializedNotification: string) => getOlmUtility().sha256(serializedNotification), + getBlobHash: async (blob: Uint8Array) => { + return getOlmUtility().sha256(new Uint8Array(blob.buffer)); + }, + generateAESKey: async () => { + const aesKeyBytes = await generateKey(); + return Buffer.from(aesKeyBytes).toString('base64'); + }, + encryptWithAESKey: async (encryptionKey: string, unencryptedData: string) => { + const encryptionKeyBytes = new Uint8Array( + Buffer.from(encryptionKey, 'base64'), + ); + const unencryptedDataBytes = new TextEncoder().encode(unencryptedData); + return await encrypt(encryptionKeyBytes, unencryptedDataBytes); + }, }; export default encryptedNotifUtilsAPI; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js index 1be6611f7..bf2cef497 100644 --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -1,423 +1,429 @@ // @flow import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; import type { MessageData, RawMessageInfo } from './message-types.js'; import type { EntityText, ThreadEntity } from '../utils/entity-text.js'; import { tShape } from '../utils/validation-utils.js'; export type NotifTexts = { +merged: string | EntityText, +body: string | EntityText, +title: string | ThreadEntity, +prefix?: string | EntityText, }; export type ResolvedNotifTexts = { +merged: string, +body: string, +title: string, +prefix?: string, }; export const resolvedNotifTextsValidator: TInterface = tShape({ merged: t.String, body: t.String, title: t.String, prefix: t.maybe(t.String), }); export type NotificationsCreationData = | { +messageDatasWithMessageInfos: ?$ReadOnlyArray<{ +messageData: MessageData, +rawMessageInfo: RawMessageInfo, }>, } | { +rescindData: { threadID: string }, } | { +badgeUpdateData: { threadID: string } }; export type SenderDeviceDescriptor = | { +keyserverID: string } | { +senderDeviceID: string }; export const senderDeviceDescriptorValidator: TUnion = t.union([ tShape({ keyserverID: t.String }), tShape({ senderDeviceID: t.String }), ]); // Web notifs types export type PlainTextWebNotificationPayload = { +body: string, +prefix?: string, +title: string, +unreadCount?: number, +threadID: string, +encryptionFailed?: '1', }; export type PlainTextWebNotification = $ReadOnly<{ +id: string, ...PlainTextWebNotificationPayload, }>; export type EncryptedWebNotification = $ReadOnly<{ ...SenderDeviceDescriptor, +id: string, +encryptedPayload: string, +type: '0' | '1', }>; export type WebNotification = | PlainTextWebNotification | EncryptedWebNotification; // WNS notifs types export type PlainTextWNSNotification = { +body: string, +prefix?: string, +title: string, +unreadCount?: number, +threadID: string, +encryptionFailed?: '1', }; export type EncryptedWNSNotification = $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string, +type: '0' | '1', }>; export type WNSNotification = | PlainTextWNSNotification | EncryptedWNSNotification; // Android notifs types export type AndroidVisualNotificationPayloadBase = $ReadOnly<{ +badge?: string, +body: string, +title: string, +prefix?: string, +threadID: string, +collapseID?: string, +badgeOnly?: '0' | '1', +encryptionFailed?: '1', }>; type AndroidSmallVisualNotificationPayload = $ReadOnly<{ ...AndroidVisualNotificationPayloadBase, +messageInfos?: string, }>; type AndroidLargeVisualNotificationPayload = $ReadOnly<{ ...AndroidVisualNotificationPayloadBase, +blobHash: string, +encryptionKey: string, }>; export type AndroidVisualNotificationPayload = | AndroidSmallVisualNotificationPayload | AndroidLargeVisualNotificationPayload; type EncryptedThinThreadPayload = { +keyserverID: string, +encryptedPayload: string, +type: '0' | '1', }; type EncryptedThickThreadPayload = { +senderDeviceID: string, +encryptedPayload: string, +type: '0' | '1', }; export type AndroidVisualNotification = { +data: $ReadOnly<{ +id?: string, ... | AndroidSmallVisualNotificationPayload | AndroidLargeVisualNotificationPayload | EncryptedThinThreadPayload | EncryptedThickThreadPayload, }>, }; type AndroidThinThreadRescindPayload = { +badge: string, +rescind: 'true', +rescindID?: string, +setUnreadStatus: 'true', +threadID: string, +encryptionFailed?: string, }; type AndroidThickThreadRescindPayload = { +rescind: 'true', +setUnreadStatus: 'true', +threadID: string, +encryptionFailed?: string, }; export type AndroidNotificationRescind = { +data: | AndroidThinThreadRescindPayload | AndroidThickThreadRescindPayload | EncryptedThinThreadPayload | EncryptedThickThreadPayload, }; type AndroidKeyserverBadgeOnlyPayload = { +badge: string, +badgeOnly: '1', +encryptionFailed?: string, }; type AndroidThickThreadBadgeOnlyPayload = { +threadID: string, +badgeOnly: '1', +encryptionFailed?: string, }; export type AndroidBadgeOnlyNotification = { +data: | AndroidKeyserverBadgeOnlyPayload | AndroidThickThreadBadgeOnlyPayload | EncryptedThinThreadPayload | EncryptedThickThreadPayload, }; type AndroidNotificationWithPriority = | { +notification: AndroidVisualNotification, +priority: 'high', } | { +notification: AndroidBadgeOnlyNotification | AndroidNotificationRescind, +priority: 'normal', }; // APNs notifs types export type APNsNotificationTopic = | 'app.comm.macos' | 'app.comm' | 'org.squadcal.app'; export type APNsNotificationHeaders = { +'apns-priority'?: 1 | 5 | 10, +'apns-id'?: string, +'apns-expiration'?: number, +'apns-topic': APNsNotificationTopic, +'apns-collapse-id'?: string, +'apns-push-type': 'background' | 'alert' | 'voip', }; type EncryptedAPNsSilentNotificationsAps = { +'mutable-content': number, +'alert'?: { body: 'ENCRYPTED' }, }; export type EncryptedAPNsSilentNotification = $ReadOnly<{ ...SenderDeviceDescriptor, +headers: APNsNotificationHeaders, +encryptedPayload: string, +type: '1' | '0', +aps: EncryptedAPNsSilentNotificationsAps, }>; type EncryptedAPNsVisualNotificationAps = $ReadOnly<{ ...EncryptedAPNsSilentNotificationsAps, +sound?: string, }>; export type EncryptedAPNsVisualNotification = $ReadOnly<{ ...EncryptedAPNsSilentNotification, +aps: EncryptedAPNsVisualNotificationAps, +id: string, }>; type APNsVisualNotificationPayloadBase = { +aps: { +'badge'?: string | number, +'alert'?: string | { +body?: string, ... }, +'thread-id': string, +'mutable-content'?: number, +'sound'?: string, }, +body: string, +title: string, +prefix?: string, +threadID: string, +collapseID?: string, +encryptionFailed?: '1', }; type APNsSmallVisualNotificationPayload = $ReadOnly<{ ...APNsVisualNotificationPayloadBase, +messageInfos?: string, }>; type APNsLargeVisualNotificationPayload = $ReadOnly<{ ...APNsVisualNotificationPayloadBase, +blobHash: string, +encryptionKey: string, }>; export type APNsVisualNotification = | $ReadOnly<{ +headers: APNsNotificationHeaders, +id: string, ... | APNsSmallVisualNotificationPayload | APNsLargeVisualNotificationPayload, }> | EncryptedAPNsVisualNotification; type APNsLegacyRescindPayload = { +backgroundNotifType: 'CLEAR', +notificationId: string, +setUnreadStatus: true, +threadID: string, +aps: { +'badge': string | number, +'content-available': number, }, }; type APNsKeyserverRescindPayload = { +backgroundNotifType: 'CLEAR', +notificationId: string, +setUnreadStatus: true, +threadID: string, +aps: { +'badge': string | number, +'mutable-content': number, }, }; type APNsThickThreadRescindPayload = { +backgroundNotifType: 'CLEAR', +setUnreadStatus: true, +threadID: string, +aps: { +'mutable-content': number, }, }; export type APNsNotificationRescind = | $ReadOnly<{ +headers: APNsNotificationHeaders, +encryptionFailed?: '1', ... | APNsLegacyRescindPayload | APNsKeyserverRescindPayload | APNsThickThreadRescindPayload, }> | EncryptedAPNsSilentNotification; type APNsLegacyBadgeOnlyNotification = { +aps: { +badge: string | number, }, }; type APNsKeyserverBadgeOnlyNotification = { +aps: { +'badge': string | number, +'mutable-content': number, }, }; type APNsThickThreadBadgeOnlyNotification = { +aps: { +'mutable-content': number, }, +threadID: string, }; export type APNsBadgeOnlyNotification = | $ReadOnly<{ +headers: APNsNotificationHeaders, +encryptionFailed?: '1', ... | APNsLegacyBadgeOnlyNotification | APNsKeyserverBadgeOnlyNotification | APNsThickThreadBadgeOnlyNotification, }> | EncryptedAPNsSilentNotification; export type APNsNotification = | APNsVisualNotification | APNsNotificationRescind | APNsBadgeOnlyNotification; export type TargetedAPNsNotification = { +notification: APNsNotification, +deliveryID: string, +encryptedPayloadHash?: string, +encryptionOrder?: number, }; export type TargetedAndroidNotification = $ReadOnly<{ ...AndroidNotificationWithPriority, +deliveryID: string, +encryptionOrder?: number, }>; export type TargetedWebNotification = { +notification: WebNotification, +deliveryID: string, +encryptionOrder?: number, }; export type TargetedWNSNotification = { +notification: WNSNotification, +deliveryID: string, +encryptionOrder?: number, }; export type NotificationTargetDevice = { +cryptoID: string, +deliveryID: string, +blobHolder?: string, }; export type TargetedNotificationWithPlatform = | { +platform: 'ios' | 'macos', +targetedNotification: TargetedAPNsNotification, } | { +platform: 'android', +targetedNotification: TargetedAndroidNotification } | { +platform: 'web', +targetedNotification: TargetedWebNotification } | { +platform: 'windows', +targetedNotification: TargetedWNSNotification }; export type EncryptedNotifUtilsAPI = { +encryptSerializedNotifPayload: ( cryptoID: string, unencryptedPayload: string, encryptedPayloadSizeValidator?: ( encryptedPayload: string, type: '1' | '0', ) => boolean, ) => Promise<{ +encryptedData: EncryptResult, +sizeLimitViolated?: boolean, +encryptionOrder?: number, }>, +uploadLargeNotifPayload: ( payload: string, numberOfHolders: number, ) => Promise< | { +blobHolders: $ReadOnlyArray, +blobHash: string, +encryptionKey: string, } | { +blobUploadError: string }, >, +getNotifByteSize: (serializedNotification: string) => number, +getEncryptedNotifHash: (serializedNotification: string) => Promise, + +getBlobHash: (blob: Uint8Array) => Promise, + +generateAESKey: () => Promise, + +encryptWithAESKey: ( + encryptionKey: string, + unencrypotedData: string, + ) => Promise, }; diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js index 2b7382b3c..d82c72f25 100644 --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -1,59 +1,62 @@ // @flow import { type Config } from '../config.js'; const getConfig = (): Config => ({ resolveKeyserverSessionInvalidationUsingNativeCredentials: null, setSessionIDOnRequest: true, calendarRangeInactivityLimit: null, platformDetails: { platform: 'web', codeVersion: 70, stateVersion: 50, }, authoritativeKeyserverID: '123', olmAPI: { initializeCryptoAccount: jest.fn(), getUserPublicKey: jest.fn(), encrypt: jest.fn(), encryptAndPersist: jest.fn(), encryptNotification: jest.fn(), decrypt: jest.fn(), decryptAndPersist: jest.fn(), contentInboundSessionCreator: jest.fn(), contentOutboundSessionCreator: jest.fn(), keyserverNotificationsSessionCreator: jest.fn(), notificationsOutboundSessionCreator: jest.fn(), isContentSessionInitialized: jest.fn(), isDeviceNotificationsSessionInitialized: jest.fn(), isNotificationsSessionInitializedWithDevices: jest.fn(), getOneTimeKeys: jest.fn(), validateAndUploadPrekeys: jest.fn(), signMessage: jest.fn(), verifyMessage: jest.fn(), markPrekeysAsPublished: jest.fn(), }, sqliteAPI: { getAllInboundP2PMessages: jest.fn(), removeInboundP2PMessages: jest.fn(), processDBStoreOperations: jest.fn(), getAllOutboundP2PMessages: jest.fn(), markOutboundP2PMessageAsSent: jest.fn(), removeOutboundP2PMessage: jest.fn(), resetOutboundP2PMessagesForDevice: jest.fn(), getRelatedMessages: jest.fn(), getOutboundP2PMessagesByID: jest.fn(), searchMessages: jest.fn(), fetchMessages: jest.fn(), }, encryptedNotifUtilsAPI: { + generateAESKey: jest.fn(), + encryptWithAESKey: jest.fn(), encryptSerializedNotifPayload: jest.fn(), uploadLargeNotifPayload: jest.fn(), getEncryptedNotifHash: jest.fn(), + getBlobHash: jest.fn(), getNotifByteSize: jest.fn(), }, }); const hasConfig = (): boolean => true; export { getConfig, hasConfig }; diff --git a/native/push/encrypted-notif-utils-api.js b/native/push/encrypted-notif-utils-api.js index 5acfbd8e9..a49064013 100644 --- a/native/push/encrypted-notif-utils-api.js +++ b/native/push/encrypted-notif-utils-api.js @@ -1,42 +1,61 @@ // @flow import type { EncryptedNotifUtilsAPI } from 'lib/types/notif-types.js'; import { getConfig } from 'lib/utils/config.js'; import { commUtilsModule } from '../native-modules.js'; +import { encrypt, generateKey } from '../utils/aes-crypto-module.js'; const encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI = { encryptSerializedNotifPayload: async ( cryptoID: string, unencryptedPayload: string, encryptedPayloadSizeValidator?: ( encryptedPayload: string, type: '1' | '0', ) => boolean, ) => { const { encryptNotification } = getConfig().olmAPI; const { message: body, messageType: type } = await encryptNotification( unencryptedPayload, cryptoID, ); return { encryptedData: { body, type }, sizeLimitViolated: encryptedPayloadSizeValidator ? !encryptedPayloadSizeValidator(body, type ? '1' : '0') : false, }; }, uploadLargeNotifPayload: async () => ({ blobUploadError: 'not_implemented' }), getNotifByteSize: (serializedNotification: string) => { return commUtilsModule.encodeStringToUTF8ArrayBuffer(serializedNotification) .byteLength; }, getEncryptedNotifHash: async (serializedNotification: string) => { const notifAsArrayBuffer = commUtilsModule.encodeStringToUTF8ArrayBuffer( serializedNotification, ); return commUtilsModule.sha256(notifAsArrayBuffer); }, + getBlobHash: async (blob: Uint8Array) => { + return commUtilsModule.sha256(blob.buffer); + }, + generateAESKey: async () => { + const aesKeyBytes = await generateKey(); + return await commUtilsModule.base64EncodeBuffer(aesKeyBytes.buffer); + }, + encryptWithAESKey: async (encryptionKey: string, unencryptedData: string) => { + const [encryptionKeyBytes, unencryptedDataBytes] = await Promise.all([ + commUtilsModule.base64DecodeBuffer(encryptionKey), + commUtilsModule.encodeStringToUTF8ArrayBuffer(unencryptedData), + ]); + + return await encrypt( + new Uint8Array(encryptionKeyBytes), + new Uint8Array(unencryptedDataBytes), + ); + }, }; export default encryptedNotifUtilsAPI; diff --git a/web/push-notif/encrypted-notif-utils-api.js b/web/push-notif/encrypted-notif-utils-api.js index e9e865688..1066b14d8 100644 --- a/web/push-notif/encrypted-notif-utils-api.js +++ b/web/push-notif/encrypted-notif-utils-api.js @@ -1,38 +1,61 @@ // @flow +import { + generateKeyCommon, + encryptCommon, +} from 'lib/media/aes-crypto-utils-common.js'; import type { EncryptedNotifUtilsAPI } from 'lib/types/notif-types.js'; import { getConfig } from 'lib/utils/config.js'; const encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI = { encryptSerializedNotifPayload: async ( cryptoID: string, unencryptedPayload: string, encryptedPayloadSizeValidator?: ( encryptedPayload: string, type: '1' | '0', ) => boolean, ) => { const { encryptNotification } = getConfig().olmAPI; const { message: body, messageType: type } = await encryptNotification( unencryptedPayload, cryptoID, ); return { encryptedData: { body, type }, sizeLimitViolated: encryptedPayloadSizeValidator ? !encryptedPayloadSizeValidator(body, type ? '1' : '0') : false, }; }, uploadLargeNotifPayload: async () => ({ blobUploadError: 'not_implemented' }), getNotifByteSize: (serializedNotification: string) => { return new Blob([serializedNotification]).size; }, getEncryptedNotifHash: async (serializedNotification: string) => { const notificationBytes = new TextEncoder().encode(serializedNotification); const hashBytes = await crypto.subtle.digest('SHA-256', notificationBytes); return btoa(String.fromCharCode(...new Uint8Array(hashBytes))); }, + getBlobHash: async (blob: Uint8Array) => { + const hashBytes = await crypto.subtle.digest('SHA-256', blob.buffer); + return btoa(String.fromCharCode(...new Uint8Array(hashBytes))); + }, + generateAESKey: async () => { + const aesKeyBytes = await generateKeyCommon(crypto); + return Buffer.from(aesKeyBytes).toString('base64'); + }, + encryptWithAESKey: async (encryptionKey: string, unencryptedData: string) => { + const encryptionKeyBytes = new Uint8Array( + Buffer.from(encryptionKey, 'base64'), + ); + const unencryptedDataBytes = new TextEncoder().encode(unencryptedData); + return await encryptCommon( + crypto, + encryptionKeyBytes, + unencryptedDataBytes, + ); + }, }; export default encryptedNotifUtilsAPI;