Skip to content

Commit

Permalink
[NEW] E2E Encryption push (iOS) (#2463)
Browse files Browse the repository at this point in the history
* link pods to notification service

* push encryption poc

* decrypt room key poc

* read user key from mmkv and cast into a pkcs

* push decrypt poc (iOS)

* expose needed watermelon methods

* watermelon -> database

* indent & simple-crypto update

* string extensions

* storage

* toBase64 -> toData

* remove a forced unwrap

* remove unused import

* database driver

* improvement

* folder structure & watermelon bridge

* more improvement stuff

* watermelon -> database

* reuse database instance

* improvement

* database fix: bypass watermelon cache

* some code improvements

* encryption instances

* start api stuff

* network layer

* improve notification service

* improve folder structure

* watermelon patch

* retry fetch logic

* rocketchat class

* fix try to decrypt without a roomKey

* fallback to original content that is translated

* some fixes to rocketchat logic

* merge develop

* remove unnecessary extension

* [CHORE] Improve reply notification code (iOS)

* undo sign changes

* remove mocked value

* import direct from library

* send message request

* reply notification with encrypted message working properly

* revert apple sign

* fix api onerror

* trick to display sender name on group notifications

* revert data.host change

* fix some multithread issues

* use sendername sent by server

* small improvement

* Bump crypto lib

* Update ios/NotificationService/NotificationService.swift

* add experimental string

* remove trailing slash

* remove trailing slash on reply

* fix decrypt messages

Co-authored-by: Diego Mello <[email protected]>
  • Loading branch information
djorkaeffalexandre and diegolmello authored Sep 24, 2020
1 parent f30c405 commit 60dc128
Show file tree
Hide file tree
Showing 68 changed files with 31,843 additions and 22,178 deletions.
6 changes: 4 additions & 2 deletions app/lib/encryption/encryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class Encryption {
}

// Initialize Encryption client
initialize = () => {
initialize = (userId) => {
this.userId = userId;
this.roomInstances = {};

// Don't await these promises
Expand All @@ -69,6 +70,7 @@ class Encryption {

// Stop Encryption client
stop = () => {
this.userId = null;
this.privateKey = null;
this.roomInstances = {};
// Cancel ongoing encryption/decryption requests
Expand Down Expand Up @@ -199,7 +201,7 @@ class Encryption {

// If doesn't have a instance of this room
if (!this.roomInstances[rid]) {
this.roomInstances[rid] = new EncryptionRoom(rid);
this.roomInstances[rid] = new EncryptionRoom(rid, this.userId);
}

const roomE2E = this.roomInstances[rid];
Expand Down
3 changes: 2 additions & 1 deletion app/lib/encryption/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import database from '../database';
import log from '../../utils/log';

export default class EncryptionRoom {
constructor(roomId) {
constructor(roomId, userId) {
this.ready = false;
this.roomId = roomId;
this.userId = userId;
this.establishing = false;
this.readyPromise = new Deferred();
this.readyPromise.then(() => {
Expand Down
4 changes: 2 additions & 2 deletions app/sagas/encryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const handleEncryptionInit = function* handleEncryptionInit() {
}

// Decrypt all pending messages/subscriptions
Encryption.initialize();
Encryption.initialize(user.id);
} catch (e) {
log(e);
}
Expand Down Expand Up @@ -109,7 +109,7 @@ const handleEncryptionDecodeKey = function* handleEncryptionDecodeKey({ password
yield Encryption.persistKeys(server, publicKey, privateKey);

// Decrypt all pending messages/subscriptions
Encryption.initialize();
Encryption.initialize(user.id);

// Hide encryption banner
yield put(encryptionSetBanner());
Expand Down
7 changes: 5 additions & 2 deletions ios/NotificationService/NotificationService-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "SecureStorage.h"
#import <MMKVAppExtension/MMKV.h>
#import <MMKV/MMKV.h>
#import <react-native-mmkv-storage/SecureStorage.h>
#import <react-native-simple-crypto/Aes.h>
#import <react-native-simple-crypto/Rsa.h>
#import <react-native-simple-crypto/Shared.h>
206 changes: 42 additions & 164 deletions ios/NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
@@ -1,177 +1,55 @@
import CoreLocation
import UserNotifications

struct PushResponse: Decodable {
let success: Bool
let data: Data

struct Data: Decodable {
let notification: Notification

struct Notification: Decodable {
let notId: Int
let title: String
let text: String
let payload: Payload

struct Payload: Decodable, Encodable {
let host: String
let rid: String?
let type: String?
let sender: Sender?
let messageId: String
let notificationType: String?
let name: String?
let messageType: String?

struct Sender: Decodable, Encodable {
let _id: String
let username: String
let name: String?
}
}
}
}
}

class NotificationService: UNNotificationServiceExtension {

var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?

var retryCount = 0
var retryTimeout = [1.0, 3.0, 5.0, 10.0]

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

if let bestAttemptContent = bestAttemptContent {
let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)!
guard let data = try? (JSONDecoder().decode(PushResponse.Data.Notification.Payload.self, from: ejson)) else {
return
}

let notificationType = data.notificationType ?? ""

// If the notification have the content at her payload, show it
if notificationType != "message-id-only" {
contentHandler(bestAttemptContent)
return
}

let mmapID = "default"
let instanceID = "com.MMKV.\(mmapID)"
let secureStorage = SecureStorage()
var cryptKey: Data = Data()
// get mmkv instance password from keychain
secureStorage.getSecureKey(instanceID.toHex()) { (response) -> () in
guard let password = response?[1] as? String else {
// kill the process and show notification as it came from APN
exit(0)
}
cryptKey = password.data(using: .utf8)!
}

// Get App Group directory
let suiteName = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
guard let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) else {
return
}

// Set App Group dir
MMKV.initialize(rootDir: nil, groupDir: directory.path, logLevel: MMKVLogLevel.none)
guard let mmkv = MMKV(mmapID: mmapID, cryptKey: cryptKey, mode: MMKVMode.multiProcess) else {
return
}

var server = data.host
if (server.last == "/") {
server.removeLast()
}
let msgId = data.messageId

let userId = mmkv.string(forKey: "reactnativemeteor_usertoken-\(server)") ?? ""
let token = mmkv.string(forKey: "reactnativemeteor_usertoken-\(userId)") ?? ""

if userId.isEmpty || token.isEmpty {
contentHandler(bestAttemptContent)
return
}

var urlComponents = URLComponents(string: "\(server)/api/v1/push.get")!
let queryItems = [URLQueryItem(name: "id", value: msgId)]
urlComponents.queryItems = queryItems

var request = URLRequest(url: urlComponents.url!)
request.httpMethod = "GET"
request.addValue(userId, forHTTPHeaderField: "x-user-id")
request.addValue(token, forHTTPHeaderField: "x-auth-token")

runRequest(request: request, bestAttemptContent: bestAttemptContent, contentHandler: contentHandler)

var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var rocketchat: RocketChat?

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

if let bestAttemptContent = bestAttemptContent {
let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)!
guard let data = try? (JSONDecoder().decode(Payload.self, from: ejson)) else {
return
}

rocketchat = RocketChat.instanceForServer(server: data.host.removeTrailingSlash())

// If the notification has the content on the payload, show it
if data.notificationType != .messageIdOnly {
self.processPayload(payload: data)
return
}

// Request the content from server
rocketchat?.getPushWithId(data.messageId) { notification in
if let notification = notification {
self.bestAttemptContent?.title = notification.title
self.bestAttemptContent?.body = notification.text
self.processPayload(payload: notification.payload)
}
}
}
}

func runRequest(request: URLRequest, bestAttemptContent: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void) {
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in

func retryRequest() {
// if we can try again
if self.retryCount < self.retryTimeout.count {
// Try again after X seconds
DispatchQueue.main.asyncAfter(deadline: .now() + self.retryTimeout[self.retryCount], execute: {
self.runRequest(request: request, bestAttemptContent: bestAttemptContent, contentHandler: contentHandler)
self.retryCount += 1
})
func processPayload(payload: Payload) {
// If is a encrypted message
if payload.messageType == .e2e {
if let message = payload.msg, let rid = payload.rid {
if let decryptedMessage = rocketchat?.decryptMessage(rid: rid, message: message) {
bestAttemptContent?.body = decryptedMessage
if let roomType = payload.type, roomType == .group, let sender = payload.senderName {
bestAttemptContent?.body = "\(sender): \(decryptedMessage)"
}
}

// If some error happened
if error != nil {
retryRequest()

// Check if the request did successfully
} else if let response = response as? HTTPURLResponse {
// if it not was successfully
if response.statusCode != 200 {
retryRequest()

// If the response status is 200
} else {
// Process data
if let data = data {
// Parse data of response
let push = try? (JSONDecoder().decode(PushResponse.self, from: data))
if let push = push {
if push.success {
bestAttemptContent.title = push.data.notification.title
bestAttemptContent.body = push.data.notification.text

let payload = try? (JSONEncoder().encode(push.data.notification.payload))
if let payload = payload {
bestAttemptContent.userInfo["ejson"] = String(data: payload, encoding: .utf8) ?? "{}"
}

// Show notification with the content modified
contentHandler(bestAttemptContent)
return
}
}
}
retryRequest()
}
}
}

task.resume()
}

override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
if let bestAttemptContent = bestAttemptContent {
contentHandler?(bestAttemptContent)
}

}
}
48 changes: 0 additions & 48 deletions ios/NotificationService/SecureStorage.h

This file was deleted.

Loading

0 comments on commit 60dc128

Please sign in to comment.