diff --git a/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js b/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js
--- a/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js
+++ b/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js
@@ -27,11 +27,23 @@
     pushType: NotificationPushType;
     threadId: string;
     payload: any;
-    badge: number;
+    badge: ?number;
     sound: string;
     contentAvailable: boolean;
     mutableContent: boolean;
     urlArgs: string[];
+    // Detailed explanation of this field can be found here:
+    // https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification?language=objc#2943363
+    // More fields can be added here, if they ever need to 
+    // be accessed from apn.Notification instance
+    aps: {
+      +badge: string | number,
+      +alert: string,
+      +'thread-id': string,
+      +'mutable-content': boolean,
+      +sound: string,
+      ...
+    };
   }
 
   declare type ProviderToken = {|
diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js
new file mode 100644
--- /dev/null
+++ b/keyserver/src/push/crypto.js
@@ -0,0 +1,78 @@
+// @flow
+
+import apn from '@parse/node-apn';
+import invariant from 'invariant';
+
+import { encryptAndUpdateOlmSession } from '../updaters/olm-session-updater.js';
+
+async function encryptIOSNotification(
+  cookieID: string,
+  notification: apn.Notification,
+): Promise<apn.Notification> {
+  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,
+  };
+
+  let encryptedSerializedPayload;
+  try {
+    const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
+    const { serializedPayload } = await encryptAndUpdateOlmSession(
+      cookieID,
+      'notifications',
+      {
+        serializedPayload: unencryptedSerializedPayload,
+      },
+    );
+    encryptedSerializedPayload = serializedPayload;
+  } 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 encryptedNotification;
+  }
+
+  encryptedNotification.payload.encryptedPayload =
+    encryptedSerializedPayload.body;
+  return encryptedNotification;
+}
+
+function prepareEncryptedIOSNotifications(
+  cookieIDs: $ReadOnlyArray<string>,
+  notification: apn.Notification,
+): Promise<Array<apn.Notification>> {
+  const notificationPromises = cookieIDs.map(cookieID =>
+    encryptIOSNotification(cookieID, notification),
+  );
+  return Promise.all(notificationPromises);
+}
+
+export { prepareEncryptedIOSNotifications };