Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
This is the source code for the Cloakroom iOS app's private chat feature. It's end-to-end cryptography is powered by Virgil Security's open source cryptographic library. All encryption and decryption is handled here on the front-end. The users' private encryption keys never leave the local device.
//
// ChatPostDetailViewController.swift
// Cloakroom
//
// Created by Theodore Henderson on 3/31/16.
// Copyright © 2016 Capitol Bells, Inc. All rights reserved.
//
import SwiftyJSON
private extension Selector {
static let newChat =
#selector(ChatPostDetailViewController.handleRefresh)
}
class ChatPostDetailViewController: PostDetailViewController {
// MARK: - Outlets
@IBOutlet weak var navItem: UINavigationItem!
@IBOutlet weak var optionsButton: UIBarButtonItem!
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var encryptionButton: UIBarButtonItem!
// MARK : - Actions
@IBAction func optionsActionTUI(_ sender: AnyObject) {
presentChatOptions()
}
@IBAction func backButtonTUI(_ sender: AnyObject) {
//deleteCounterpartyChats()
navigationController?.popViewController(animated: true)
}
@IBAction func encryptionAction(_ sender: UIBarButtonItem) {
if CurrentUser.hasBeenAskedToEnableEncryption().get() == false {
CurrentUser.hasBeenAskedToEnableEncryption().set()
enableEncryptionAlert()
} else {
encryptionActionLogic(sender)
}
}
// MARK : - Variables
var intro: IntroModel?
var chatArray = [ChatModel]()
var blockReasonTextField: UITextField!
var firstLoad = true
var privateKey: Data?
var publicKey: Data?
var chatVisible = false
//MARK: - Override Functions
override func viewDidLoad() {
super.viewDidLoad()
//populateRandomChats()
navigationController?.navigationBar.barTintColor = Constants.Colors.appDarkBlue
showHUDOnView(containerView, text: "Loading")
getChatsForIntro(refresh: false)
watchForScreenCapture()
getEncryptionKeys()
postDetailTableView.addSubview(refreshControl)
autofillTableViewSetup()
setScrollViewsScrollToTap()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
chatVisible = true
DispatchQueue.main.async(execute: {self.checkIntroForEncryptionState(self.intro)})
}
override func viewDidDisappear(_ animated: Bool) {
chatVisible = false
}
override func commentTableViewSetup() {
if intro?.counterpartyAlias != nil {
navItem.title = "\(intro!.counterpartyAlias!)"
}
placeholderText = "As \(intro!.myAlias!)"
postDetailTableView.register(UINib(nibName: "ChatCommentCell", bundle: nil), forCellReuseIdentifier: Constants.CellIdentifer.chatCell)
postDetailTableView.rowHeight = UITableViewAutomaticDimension
postDetailTableView.estimatedRowHeight = 120.0
NotificationCenter.default.addObserver(self, selector: Selector.newChat, name:NSNotification.Name(rawValue: "NewChat"), object: nil)
}
override func setAliasPlaceHolderText() {
placeholderText = "As \(intro!.myAlias!)"
commentTextView.text = placeholderText
}
override func longCommentAlert() {
self.displayHelperAlert("Chat messages must be less than \(Constants.CharacterLimits.maxCommentLength) characters long.")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView.tag == 99 {
if chatArray.count > 0 {
return chatArray.count
} else if firstLoad == true {
return 0
} else {
return 1
}
} else {
return filteredAutofillAliasArray.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView.tag == 99 {
if chatArray.count > 0 {
let chatCell = tableView.dequeueReusableCell(withIdentifier: Constants.CellIdentifer.chatCell, for: indexPath) as! ChatCommentTableViewCell
let chatIndex = indexPath.row
let chat = chatArray[chatIndex]
chatCell.setupChatCell(chat)
chatCell.formatCommentCellUI()
chatCell.commentTableViewCellDelegate = self
return chatCell
} else {
// Return empty cell here
let emptyCell = tableView.dequeueReusableCell(withIdentifier: "emptyChatCell", for: indexPath)
return emptyCell
}
} else {
let autofillCell = tableView.dequeueReusableCell(withIdentifier: "autofillAliasCell")! as UITableViewCell
autofillCell.textLabel?.text = filteredAutofillAliasArray[indexPath.row]
return autofillCell
}
}
override func sendCommentLogic(_ sender: UIButton) {
askForCommentResponsePushIfNecessaryAndComment(UIApplication.shared, sender: sender)
commentTextView.resignFirstResponder()
}
override func attemptSendComment(_ sender: UIButton) {
super.activeTextViewHeight.constant = defaultTextViewHeight
let commentText = commentTextView.text
if commentText!.characters.count > 0 && commentText!.characters.count <= Constants.CharacterLimits.maxCommentLength && !isStringEmpty(commentText!) && commentText != placeholderText {
if intro?.isEncrypted == false {
sendChatAPIHandler(sender, chatText: commentText!)
} else {
sendEncryptedChatAPIHandler(sender, chatText: commentText!)
}
} else if commentTextView.text.characters.count > Constants.CharacterLimits.maxCommentLength {
longCommentAlert()
print("Long comment alert")
} else {
print("What happened here??")
}
}
override func didDeleteCommentWithKey(_ commentKeyToDelete: String) {
// CommentKey here is actually a ChatKey
showHUDForDuration("Message Deleted")
var i = 0
for chat in chatArray {
if chat.chatKey == commentKeyToDelete {
chatArray.remove(at: i)
postDetailTableView.reloadData()
break
}
i += 1
}
}
//MARK: - Controls
override func addPostAuthorAndCommentersToMentionsArray() {
// THis is chat, so only add coutnerparty alias here
if intro?.counterpartyAlias != nil {
if !originalAutofillAliasArray.contains((intro?.counterpartyAlias)!) {
originalAutofillAliasArray.append((intro?.counterpartyAlias)!)
}
}
}
override func getCommentsForPostShowLoadingIndicator() {
//nothing
}
override func handleRefresh() {
// Get chats for intro API call
if intro != nil {
getChatsForIntro(refresh: true)
}
}
// func populateRandomChats() {
// var i = 0
// while i < 10 {
// let newChat = ChatModel.init(NSNumber(i))
//
// chatArray.append(newChat)
// i += 1
// }
// }
func presentChatOptions() {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
// ACTION 1
let viewProfileAction = UIAlertAction(title: "View Profile", style: .default) { (action) in
self.segueToCounterpartyProfile()
}
actionSheet.addAction(viewProfileAction)
// ACTION 2
if checkIntroForEncryptionState(intro) == true {
var encryptionTitle = "Turn Encryption On"
if intro?.isEncrypted == true {encryptionTitle = "Turn Encryption Off"}
let encryptionAction = UIAlertAction(title: encryptionTitle, style: .default) { (action) in
self.enableEncryptionAlert()
}
actionSheet.addAction(encryptionAction)
}
// ACTION 3
let clearAction = UIAlertAction(title: "Wipe Conversation", style: .default) { (action) in
self.deleteAllChats()
}
actionSheet.addAction(clearAction)
// ACTION 4
let blockAction = UIAlertAction(title: "Block User", style: .destructive) { (action) in
self.blockUserAlert()
}
actionSheet.addAction(blockAction)
// ACTION 5
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action) in
// do nothing
}
actionSheet.addAction(cancelAction)
if actionSheet.popoverPresentationController != nil {
actionSheet.popoverPresentationController!.sourceView = self.view
actionSheet.popoverPresentationController!.sourceRect = CGRect(x: self.view.bounds.size.width / 2.0, y: view.bounds.size.height / 2.0, width: 1.0, height: 1.0)
}
DispatchQueue.main.async(execute: {self.present(actionSheet, animated: true, completion: nil)})
}
func blockUserAlert() {
present(
AlertViewManager.blockUserAlert({ (textField) in
//handle the contents of the textField
textField.placeholder = "Reason for blocking"
self.blockReasonTextField = textField
}, targetAction: blockAlertAction()), animated: true, completion: nil)
}
func blockAlertAction() -> UIAlertAction {
let blockAction = UIAlertAction(title: "Block", style: .destructive, handler: {(action) in
if self.blockReasonTextField.text == nil {
self.blockUser(reason: "")
} else {
self.blockUser(reason: self.blockReasonTextField.text!)
}
})
return blockAction
}
// MARK: - Encryption Logic
func enableEncryptionAlert() {
present(
AlertViewManager.defaultActionAlert(title: "Enable Encryption?", message: "Secure this conversation by enabling end-to-end encryption. You will only be able to read these encrypted messages from this device.", style: .alert, action: EnableEncryptionAlertAction()), animated: true, completion: nil)
}
func EnableEncryptionAlertAction() -> UIAlertAction {
let encryptAction = UIAlertAction(title: "Encrypt", style: .destructive, handler: {(action) in
self.toggleEncryptionEndpoint(true)
})
return encryptAction
}
func encryptionActionLogic(_ sender: UIBarButtonItem) {
if intro!.isEncrypted == true {
toggleEncryptionEndpoint(false)
} else {
enableEncryptionAlert()
}
}
func toggleEncryptionUI(_ encrypted: Bool) {
if encrypted == true {
encryptionButton.title = "🔒"
navigationController?.navigationBar.barTintColor = Constants.Colors.appDarkGrey
} else {
encryptionButton.title = "🔓"
navigationController?.navigationBar.barTintColor = Constants.Colors.appDarkBlue
}
}
func checkPublicKeyConsistency() {
if intro?.myPublicKey != publicKey {
updateEncryptionKeyForIntro(intro!.introKey!)
}
}
func getEncryptionKeys() {
publicKey = CurrentUser.encryptionKeys().getPublicKey() as Data?
privateKey = CurrentUser.encryptionKeys().getPrivateKey() as Data?
if publicKey == nil || privateKey == nil {
let newKeyPair = CurrentUser.encryptionKeys().generateNewKeyPair()
privateKey = newKeyPair.privateKey()
publicKey = newKeyPair.publicKey()
}
}
func encryptMessage(_ message: String) -> Data? {
let message = NSString(string: message)
// Convert message String to NSData
if let toEncrypt = message.data(using: String.Encoding.utf8.rawValue, allowLossyConversion: false) {
let cryptor = VSSCryptor()
do {
try cryptor.addKeyRecipient(intro!.counterpartyAlias!, publicKey: intro!.counterpartyPublicKey! as Data, error: ())
// Encrypting the message data using counterparty's public key
var encryptedData = Data()
encryptedData = try cryptor.encryptData(toEncrypt, embedContentInfo: true, error: ())
return encryptedData
}
catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
//...
}
return nil
}
func decryptMessage(_ encryptedData: Data) -> String? {
let decryptor = VSSCryptor()
do {
// Decrypting the data with current user's private key
let decryptedData = try decryptor.decryptData(encryptedData, recipientId: intro!.myAlias!, privateKey: privateKey!, keyPassword: nil, error: ())
// Convert the decrypted data to a String
if let decryptedMessage = NSString(data: decryptedData, encoding: String.Encoding.utf8.rawValue) {
return decryptedMessage as String
} else {
return nil
}
}
catch let error as NSError {
print("Error: \(error.localizedDescription)")
return nil
}
}
func checkIntroForEncryptionState(_ currentIntro: IntroModel?) -> Bool {
// check if is encrypted
// if yes, toggle encryption UI
if currentIntro!.myPublicKey == nil && currentIntro != nil {
updateEncryptionKeyForIntro(currentIntro!.introKey!)
disableAndHideEncryptionButton()
return false
} else if currentIntro!.counterpartyPublicKey?.count == 0 {
disableAndHideEncryptionButton()
return false
} else {
encryptionButton.isEnabled = true
if currentIntro != nil {toggleEncryptionUI(currentIntro!.isEncrypted!)}
return true
}
}
func disableAndHideEncryptionButton() {
encryptionButton.isEnabled = false
encryptionButton.title = ""
intro!.isEncrypted = false
}
//MARK: - Screen Capture Logic
func watchForScreenCapture() {
NotificationCenter.default.addObserver(
forName: NSNotification.Name.UIApplicationUserDidTakeScreenshot,
object: nil,
queue: OperationQueue.main)
{
notification in
self.screenCaptureDetected()
}
}
func screenCaptureDetected() {
if self == navigationController?.visibleViewController && chatVisible == true {
if let counterpartyAlias = intro?.counterpartyAlias {
if chatArray.count > 0 {
reportScreenCaptureToBackend(counterpartyAlias)
}
}
} else {
// This isn't the visible view controller, so ignore this.
}
}
func reportScreenCaptureToBackend(_ counterpartyAlias: String) {
RequestManager.alertByScreenshot(CurrentUser.key().get()!, introKey: (intro?.introKey)!, offenderAlias: (intro?.myAlias)!, counterpartyAlias: counterpartyAlias, success: {(data) in
self.reportedScreenCaptureSuccess(counterpartyAlias)
}, failure: {(error) in
print("Failure reporting screenshot to backend.")
self.failureGetPostDetails(error)
})
}
func reportedScreenCaptureSuccess(_ counterpartyAlias: String) {
displayHelperAlertDefault("Shame", message: "Shame. Shame. We've notified \(counterpartyAlias) that you took a screenshot.")
}
//MARK: - API Calls
func getChatsForIntro(refresh shouldRefresh: Bool) {
RequestManager.getChatMessages(CurrentUser.key().get()!, introKey: (intro?.introKey)!, recipient: (intro?.counterpartyAlias)!, sender: (intro?.myAlias)!, success: {(data) in
//print(data)
self.refreshControl.endRefreshing()
self.hideHUDOnView(self.containerView)
self.firstLoad = false
self.successGetChats(data, refresh: shouldRefresh)
}, failure: {(error) in
self.refreshControl.endRefreshing()
self.firstLoad = false
self.hideHUDOnView(self.postDetailTableView)
})
}
func sendChatAPIHandler(_ sender: UIButton, chatText: String) {
self.showHUDOnView(postDetailTableView, text: "")
sender.isEnabled = false
RequestManager.sendChatMessage(CurrentUser.key().get()!, forIntro: intro!.introKey!, from: intro!.myAlias!, to: (intro?.counterpartyAlias!)!, message: chatText, success: {(data) -> Void in
self.hideHUDOnView(self.postDetailTableView)
sender.isEnabled = true
self.successSentChat(data, chatText: chatText)
}, failure: {error in
self.hideHUDOnView(self.postDetailTableView)
self.failureGetPostDetails(error)
})
}
func sendEncryptedChatAPIHandler(_ sender: UIButton, chatText: String) {
self.showHUDOnView(postDetailTableView, text: "🔒")
sender.isEnabled = false
let encryptedData = encryptMessage(chatText)
RequestManager.sendEncryptedChatMessage(userKey: CurrentUser.key().get()!, introKey: (intro?.introKey!)!, sender: (intro?.myAlias!)!, recipient: (intro?.counterpartyAlias!)!, encryptedData: encryptedData!, success: { (data) -> Void in
self.hideHUDOnView(self.postDetailTableView)
sender.isEnabled = true
//print(data)
self.successSentChat(data, chatText: chatText)
}) { (error) -> Void in
self.hideHUDOnView(self.postDetailTableView)
self.failureGetPostDetails(error)
}
}
func updateEncryptionKeyForIntro(_ introKey: String) {
var publicKey = CurrentUser.encryptionKeys().getPublicKey()
if publicKey == nil {
let keyPair = CurrentUser.encryptionKeys().generateNewKeyPair()
publicKey = keyPair.publicKey()
}
RequestManager.updateEncryptionKey(userKey: CurrentUser.key().get()!, alias: (intro?.myAlias!)!, introKey: introKey, publicKey: publicKey!, success: { (data) in
// Replace intro with returned intro object
//print(data)
self.successUpdateEncryptionKeyForIntro(data)
}) { (error) in
// Background call, do nothing on failure
print(error)
}
}
func toggleEncryptionEndpoint(_ enableEncryption: Bool) {
RequestManager.toggleEncryption(CurrentUser.key().get()!, introKey: intro!.introKey!, enable: enableEncryption, success: { (data) in
self.successToggleEncryptionEndpoint(data)
}) { (error) in
self.hideHUDOnView(self.postDetailTableView)
self.failureGetPostDetails(error)
}
}
// MARK: API Call Response Handlers
func successGetChats(_ data: (AnyObject!), refresh: Bool) {
var json = JSON(data!)
let result = json["result"].string!
if (result == "ok") {
//print(privateKey)
for (_, object) in json["chats"] {
let chat = ChatModel(object: object)
if isDuplicateChat(chat) == false {
chatArray.append(chat)
}
if chat.encrypted_message != "" {
let encryptedData = chat.encrypted_message!.data(using: String.Encoding.isoLatin1)
chat.message = decryptMessage(encryptedData!)
if chat.message == nil {
chat.message = "***ENCRYPTED MESSAGE***"
}
chat.encrypted_message = ""
chat.isEncrypted = true
} else {
chat.isEncrypted = false
}
}
chatArray = chatArray.sorted(by: { $0.timestamp!.intValue < $1.timestamp!.intValue})
postDetailTableView.reloadData()
let numberOfRows = self.postDetailTableView.numberOfRows(inSection: 0)
if numberOfRows > 0 {
self.scrollTableToBottom(IndexPath(row: numberOfRows-1, section: 0))
}
checkPublicKeyConsistency()
encryptionButton.isEnabled = true
} else {
let message = json["message"].string!
self.displayErrorAlert(message)
}
}
func successToggleEncryptionEndpoint(_ data: (AnyObject!)) {
var json = JSON(data!)
let result = json["result"].string!
if (result == "ok") {
//print(json["intro"])
self.intro = IntroModel(object: json["intro"])
self.checkIntroForEncryptionState(self.intro)
if intro?.isEncrypted == true {
showHUDForDuration("Encryption ON")
} else {
showHUDForDuration("Encryption OFF")
}
} else {
//let message = json["message"].string!
//print(json)
//print(message)
}
}
func successUpdateEncryptionKeyForIntro(_ data: (AnyObject!)) {
var json = JSON(data!)
let result = json["result"].string!
if (result == "ok") {
self.intro = IntroModel(object: json["intro"])
self.toggleEncryptionUI((self.intro?.isEncrypted)!)
} else {
let message = json["message"].string!
self.displayErrorAlert(message)
}
}
func successSentChat(_ data: (AnyObject!), chatText: String) {
var json = JSON(data!)
let result = json["result"].string!
if (result == "ok") {
let myNewChat = ChatModel(object: json["chat"])
if myNewChat.encrypted_message != "" {
myNewChat.encrypted_message = ""
myNewChat.message = chatText
myNewChat.isEncrypted = true
} else {
myNewChat.isEncrypted = false
}
chatArray.append(myNewChat)
chatArray = chatArray.sorted(by: { $0.timestamp!.intValue < $1.timestamp!.intValue})
postDetailTableView.reloadData()
commentTextView.text = ""
activeTextViewHeight.constant = defaultTextViewHeight
characterCountLabel.isHidden = true
let numberOfRows = self.postDetailTableView.numberOfRows(inSection: 0)
if numberOfRows > 0 {
scrollTableToBottom(IndexPath(row: numberOfRows-1, section: 0))
}
} else {
let message = json["message"].string!
self.displayErrorAlert(message)
}
}
func getChatKeysToDelete() -> [String] {
var chatKeysToDeleteArray = [String]()
for chat in chatArray {
chatKeysToDeleteArray.append(chat.chatKey!)
}
return chatKeysToDeleteArray
}
func deleteIntro() {
RequestManager.deleteIntro(CurrentUser.key().get()!, intro: (intro?.introKey!)!, success: { (data) -> Void in
self.hideHUD()
self.successDeleteIntroJSONResponseHandler(data)
}) { (error) -> Void in
self.hideHUD()
self.displayErrorAlert((error?.localizedDescription)!)
}
}
func blockUser(reason reasonToBlock: String) {
RequestManager.blockChat(CurrentUser.key().get()!, aliasToBlock: (intro?.counterpartyAlias!)!, reasonForBlocking: reasonToBlock, success: { (data) -> Void in
self.hideHUD()
self.successDeleteIntroJSONResponseHandler(data)
}) { (error) -> Void in
self.hideHUD()
self.displayErrorAlert((error?.localizedDescription)!)
}
}
func deleteAllChats() {
let chatKeysToDelete: [String] = getChatKeysToDelete()
if chatKeysToDelete.count > 0 {
RequestManager.deleteChatMessages(CurrentUser.key().get()!, chatKeys: chatKeysToDelete, success: { (data) -> Void in
self.successDeleteCounterpartyChatsJSONResponseHandler(data)
}) { (error) -> Void in
if error != nil {
self.displayErrorAlert(error!.localizedDescription)
} else {
print("Error: \(error)")
}
}
} else {
// No chats to delete
}
}
override func getUserFollows() {
RequestManager.getUserFollows(userKey: CurrentUser.key().get()!, success: { (data) in
let json = JSON(data)
let result = json["result"]
if result == "ok" {
for alias in json["follows"].arrayObject as! [String] {
if !self.originalAutofillAliasArray.contains(alias) {
if CurrentUser().isAliasInAliasArray(alias) == false {
self.originalAutofillAliasArray.insert(alias, at: 0)
}
}
}
self.originalAutofillAliasArray = self.originalAutofillAliasArray.reversed()
}
}) { (error) in
self.displayErrorAlert(error!.localizedDescription)
}
}
func successDeleteIntroJSONResponseHandler(_ data: (AnyObject!)) {
let json = JSON(data!)
let result = json["result"].string!
if (result == "ok") {
showHUDForDuration("Cleared")
navigationController?.popViewController(animated: true)
} else {
hideHUD()
displayErrorAlert(json["message"].string!)
//showHUDForDuration("Error. Failed to delete.")
}
}
func successDeleteCounterpartyChatsJSONResponseHandler(_ data: (AnyObject!)) {
let json = JSON(data!)
//print(json)
let result = json["result"].string!
if (result == "ok") {
//print("Post successfully deleted.")
chatArray.removeAll()
postDetailTableView.reloadData()
} else {
self.displayErrorAlert(json["message"].string!)
//showHUDForDuration("Error. Failed to delete.")
}
}
func isDuplicateChat(_ newChat: ChatModel) -> Bool {
for existingChat in chatArray {
if existingChat.chatKey == newChat.chatKey {
return true
}
}
return false
}
//MARK: - Navigation Methods
func segueToCounterpartyProfile() {
let counterpartyProfileVC: CounterpartyProfileViewController = self.storyboard?.instantiateViewController(withIdentifier: "counterpartyProfileVC") as! CounterpartyProfileViewController
counterpartyProfileVC.profileAlias = intro?.counterpartyAlias
navigationController?.pushViewController(counterpartyProfileVC, animated: true)
}
}
struct CurrentUser {
struct encryptionKeys {
let publicKeyIdentifier = "encodedPublicKey-\(key().get())"
let privateKeyIdentifier = "encodedPrivateKey-\(key().get())"
func getPublicKey() -> Data? {
return defaults.data(forKey: publicKeyIdentifier)
}
func getPrivateKey() -> Data? {
return defaults.data(forKey: privateKeyIdentifier)
}
func set(_ encryptedKeyPair: VSSKeyPair) {
defaults.set(encryptedKeyPair.publicKey(), forKey: publicKeyIdentifier)
defaults.set(encryptedKeyPair.privateKey(), forKey: privateKeyIdentifier)
}
func remove() {
defaults.set(nil, forKey: publicKeyIdentifier)
defaults.set(nil, forKey: privateKeyIdentifier)
}
func generateNewKeyPair() -> VSSKeyPair {
let newKeyPair = VSSKeyPair(password:nil)
set(newKeyPair)
return newKeyPair
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.