diff --git a/.gitignore b/.gitignore index 3ac667ee..9d93e82d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ out .DS_Store xcuserdata *.iml +*.jar + xcshareddata # Xcode # diff --git a/DoctorsNote/DoctorsNote.xcodeproj/project.pbxproj b/DoctorsNote/DoctorsNote.xcodeproj/project.pbxproj index 25ddf387..dc5c0c0a 100644 --- a/DoctorsNote/DoctorsNote.xcodeproj/project.pbxproj +++ b/DoctorsNote/DoctorsNote.xcodeproj/project.pbxproj @@ -25,8 +25,11 @@ 9D23056E24105ED00064E6E2 /* Reminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D23056D24105ED00064E6E2 /* Reminder.swift */; }; 9D267F0F24105CD000E787FB /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D267F0E24105CD000E787FB /* Message.swift */; }; 9D325395242A765F0057B354 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D267F0124105BDA00E787FB /* User.swift */; }; + 9D41A4A62443BA880069A1B8 /* MessageCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D41A4A52443BA880069A1B8 /* MessageCipher.swift */; }; + 9D41A4A72443BA8B0069A1B8 /* MessageCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D41A4A52443BA880069A1B8 /* MessageCipher.swift */; }; 9D90C39B2400666B006FD4C6 /* ConnectionProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90C38B23F9CF8B006FD4C6 /* ConnectionProcessorTests.swift */; }; 9DD1D469242E8B3200925298 /* Appointment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E3BA28242C258900820F05 /* Appointment.swift */; }; + 9DDB41372444072A0021FDDC /* LocalCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DDB41362444072A0021FDDC /* LocalCipher.swift */; }; AD4567D3634346E5B3E37C80 /* Pods_DoctorsNoteTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A21B97793A0B6E9F53489841 /* Pods_DoctorsNoteTests.framework */; }; B2E92EDF0D4EB4D291081AEE /* Pods_DoctorsNote_DoctorsNoteUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6F1A60D0FD3CCB8BB833746 /* Pods_DoctorsNote_DoctorsNoteUITests.framework */; }; B51C6A20242C551F00048BCF /* TestChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51C6A1F242C551F00048BCF /* TestChatView.swift */; }; @@ -116,7 +119,9 @@ 9D23056D24105ED00064E6E2 /* Reminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reminder.swift; sourceTree = ""; }; 9D267F0124105BDA00E787FB /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 9D267F0E24105CD000E787FB /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 9D41A4A52443BA880069A1B8 /* MessageCipher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCipher.swift; sourceTree = ""; }; 9D90C38B23F9CF8B006FD4C6 /* ConnectionProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionProcessorTests.swift; sourceTree = ""; }; + 9DDB41362444072A0021FDDC /* LocalCipher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCipher.swift; sourceTree = ""; }; A21B97793A0B6E9F53489841 /* Pods_DoctorsNoteTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DoctorsNoteTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B50173F82408323F003D2224 /* SearchGroupsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchGroupsTableViewController.swift; sourceTree = ""; }; B51C6A1F242C551F00048BCF /* TestChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestChatView.swift; sourceTree = ""; }; @@ -265,6 +270,8 @@ 442F306523F883A8001EBC3C /* ConversationViewController.swift */, B82083CA24033A1E00CF41D0 /* SupportGroupMessageVC.swift */, 447323FF243184900026DA34 /* SupportGroupConvo.swift */, + 9D41A4A52443BA880069A1B8 /* MessageCipher.swift */, + 9DDB41362444072A0021FDDC /* LocalCipher.swift */, ); path = DoctorsNote; sourceTree = ""; @@ -639,6 +646,7 @@ B85C61B5242DDB160002D014 /* AppointmentListCell.swift in Sources */, B54E4F262408ED8300349316 /* DefinedValues.swift in Sources */, 9D137F45242BAC2000A0B0AB /* HealthSystem.swift in Sources */, + 9D41A4A62443BA880069A1B8 /* MessageCipher.swift in Sources */, B587B55A2429710C00B6BF0D /* EditReminderVC.swift in Sources */, B54E4F3A2408EEA900349316 /* SearchGroupsTableViewController.swift in Sources */, B85C61B7242DE6950002D014 /* AppointmentListVC.swift in Sources */, @@ -656,6 +664,7 @@ B87E49042408F462000E6ABC /* RemindersVC.swift in Sources */, B52ABE6A23F5B872007B3D6A /* AppDelegate.swift in Sources */, B5C12168241DCCC400B2905A /* DoctorProfileViewController.swift in Sources */, + 9DDB41372444072A0021FDDC /* LocalCipher.swift in Sources */, B52ABE6C23F5B872007B3D6A /* SceneDelegate.swift in Sources */, B54E4F332408EDA200349316 /* ForgotPasswordViewController.swift in Sources */, B54E4F382408EDA200349316 /* PersonalInfoView.swift in Sources */, @@ -682,6 +691,7 @@ B51C6A22242C574B00048BCF /* (null) in Sources */, B51C6A23242C574B00048BCF /* (null) in Sources */, B5AE734E24156C5B00A9CDAE /* (null) in Sources */, + 9D41A4A72443BA8B0069A1B8 /* MessageCipher.swift in Sources */, 9D137F40242BAB6E00A0B0AB /* ConnectionProcessor.swift in Sources */, 9D137F43242BAB7700A0B0AB /* Conversation.swift in Sources */, 9D137F89242C020A00A0B0AB /* Reminder.swift in Sources */, diff --git a/DoctorsNote/DoctorsNote/AppDelegate.swift b/DoctorsNote/DoctorsNote/AppDelegate.swift index ad94777f..01a46a66 100644 --- a/DoctorsNote/DoctorsNote/AppDelegate.swift +++ b/DoctorsNote/DoctorsNote/AppDelegate.swift @@ -16,6 +16,11 @@ import AWSCognito import AWSMobileClient import UserNotifications +import CryptoKit +import CommonCrypto + + + extension AWSMobileClientError { var message: String { switch self { @@ -86,6 +91,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + requestNotificationAuthorization(application: application) // Override point for customization after application launch. diff --git a/DoctorsNote/DoctorsNote/Base.lproj/Main.storyboard b/DoctorsNote/DoctorsNote/Base.lproj/Main.storyboard index 35387917..334203f6 100644 --- a/DoctorsNote/DoctorsNote/Base.lproj/Main.storyboard +++ b/DoctorsNote/DoctorsNote/Base.lproj/Main.storyboard @@ -415,7 +415,7 @@ - + diff --git a/DoctorsNote/DoctorsNote/ChatLogController.swift b/DoctorsNote/DoctorsNote/ChatLogController.swift index 2e425e9c..aba425c0 100644 --- a/DoctorsNote/DoctorsNote/ChatLogController.swift +++ b/DoctorsNote/DoctorsNote/ChatLogController.swift @@ -60,11 +60,23 @@ class ChatLogController: UIViewController, UICollectionViewDelegate, UICollectio // Do any additional setup after loading the view. messagesShown = 20; do { - messages = try (connectionProcessor.processMessages(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messagelist/", conversationID: conversation!.getConversationID(), numberToRetrieve: messagesShown) ?? messages) + if conversation?.getConverserID() == "N/A" { + messages = try connectionProcessor.processMessages(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messagelist/", conversationID: conversation!.getConversationID(), numberToRetrieve: messagesShown) + + } else { + let cipher = LocalCipher() + messages = try connectionProcessor.processMessages(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messagelist/", conversationID: conversation!.getConversationID(), numberToRetrieve: messagesShown, cipher: MessageCipher(uniqueID: CognitoHelper.user!.getUID(), localAESKey: cipher.getAESFromPass(password: CognitoHelper.password!, username: CognitoHelper.user!.getUID()))) + } print(messages) + } catch let error as CipherError { + print ((error).getMessage()) + print("Cipher ERROR!!!!!!!!!!!!") } catch let error { print ((error as! ConnectionError).getMessage()) print("ERROR!!!!!!!!!!!!") + } catch let error { + print (error.localizedDescription) + print("ERROR!!!!!!!!!!!!") } let item = self.collectionView(self.collectionView, numberOfItemsInSection: 0) - 1 @@ -99,22 +111,50 @@ class ChatLogController: UIViewController, UICollectionViewDelegate, UICollectio } let newMessage = Message(messageID: -1, conversationID: conversation!.getConversationID(), content: (messageText!.text?.data(using: .utf8))!, contentType: 0, numFails: CognitoHelper.numFails) print(newMessage.getBase64Content()) - let err = connectionProcessor.processNewMessage(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messageadd", message: newMessage) - if (err != nil) { - CognitoHelper.numFails += 1 - let alertController = UIAlertController(title: "Error Sending Message", message: "The message failed to send.", preferredStyle: .alert) - let okAction = UIAlertAction(title: "Ok", style: .default, handler: nil) - alertController.addAction(okAction) - // Turn this into a reminder eventually because it takes so long to determine that the message failed to send. - self.present(alertController, animated: true, completion: nil) - } else { - // Reset the number of failed sends to 0 - CognitoHelper.numFails = 0 - } + sendMessage(toSend: newMessage) + reloadMessages() print("Pressed2") } + func sendMessage(toSend: Message) { + if conversation?.getConverserID() != "N/A" { + do { + let cipher = try MessageCipher(uniqueID: CognitoHelper.user!.getUID(), localAESKey: LocalCipher().getAESFromPass(password: CognitoHelper.password!, username: CognitoHelper.user!.getUID())) + let err = connectionProcessor.processNewMessage(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messageadd", message: toSend, cipher: cipher, publicKeyExternalBase64: conversation?.getConverserPublicKey(), adminPublicKeyExternalBase64: conversation?.getAdminPublicKey()) + if (err != nil) { + CognitoHelper.numFails += 1 + throw err! + } else { + CognitoHelper.numFails = 0 + } + } catch let error { + if error as? ConnectionError != nil { + print((error as! ConnectionError).getMessage()) + } else { + print("Error: " + error.localizedDescription) + } + let alertController = UIAlertController(title: "Error Sending Message", message: "The message failed to send.", preferredStyle: .alert) + let okAction = UIAlertAction(title: "Ok", style: .default, handler: nil) + alertController.addAction(okAction) + // Turn this into a reminder eventually because it takes so long to determine that the message failed to send. + self.present(alertController, animated: true, completion: nil) + } + } else { + let err = connectionProcessor.processNewMessage(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messageadd", message: toSend) + if (err != nil) { + CognitoHelper.numFails += 1 + let alertController = UIAlertController(title: "Error Sending Message", message: "The message failed to send.", preferredStyle: .alert) + let okAction = UIAlertAction(title: "Ok", style: .default, handler: nil) + alertController.addAction(okAction) + // Turn this into a reminder eventually because it takes so long to determine that the message failed to send. + self.present(alertController, animated: true, completion: nil) + } else { + CognitoHelper.numFails = 0 + } + } + } + //Credit for how to set up the ImagePickerDelagate goes to: https://www.youtube.com/watch?v=v8r_wD_P3B8 @IBAction func imageSend() { @@ -150,24 +190,31 @@ class ChatLogController: UIViewController, UICollectionViewDelegate, UICollectio print(content.base64EncodedString().count) } let newMessage = Message(messageID: -1, conversationID: conversation!.getConversationID(), content: content, contentType: 1, numFails: CognitoHelper.numFails) + + sendMessage(toSend: newMessage) - //print(newMessage.getContent()) - let potentialError = connectionProcessor.processNewMessage(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messageadd", message: newMessage) - if (potentialError != nil) { - print(potentialError?.getMessage()) - } dismiss(animated: false) reloadMessages() } func reloadMessages() { messagesShown += 1 - do { - messages = try connectionProcessor.processMessages(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messagelist/", conversationID: conversation!.getConversationID(), numberToRetrieve: messagesShown) - print(try connectionProcessor.processMessages(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messagelist/", conversationID: conversation!.getConversationID(), numberToRetrieve: messagesShown)) - } catch let error { - print ((error as! ConnectionError).getMessage()) - print("ERROR!!!!!!!!!!!!") + if conversation?.getConverserID() != "N/A" { + do { + let cipher = LocalCipher() + messages = try connectionProcessor.processMessages(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messagelist/", conversationID: conversation!.getConversationID(), numberToRetrieve: messagesShown, cipher: MessageCipher(uniqueID: CognitoHelper.user!.getUID(), localAESKey: cipher.getAESFromPass(password: CognitoHelper.password!, username: CognitoHelper.user!.getUID()))) + print(messages) + } catch let error { + print (error.localizedDescription) + print("ERROR!!!!!!!!!!!!") + } + } else { + do { + messages = try connectionProcessor.processMessages(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/messagelist/", conversationID: conversation!.getConversationID(), numberToRetrieve: messagesShown) + } catch let error { + print (error.localizedDescription) + print("ERROR!!!!!!!!!!!!") + } } collectionView.reloadData() } diff --git a/DoctorsNote/DoctorsNote/CognitoHelper.swift b/DoctorsNote/DoctorsNote/CognitoHelper.swift index 2fbbbf62..51166218 100644 --- a/DoctorsNote/DoctorsNote/CognitoHelper.swift +++ b/DoctorsNote/DoctorsNote/CognitoHelper.swift @@ -13,9 +13,12 @@ class CognitoHelper { public static let sharedHelper = CognitoHelper() public static var user: User? + public static var password: String? public static var numFails: Int = 0 func login(email: String, password: String, onDone: @escaping (_ success: Bool, _ err: AWSMobileClientError)->()) { + print("Password: " + password) + CognitoHelper.password = password AWSMobileClient.default().signIn(username: email, password: password) { (result, err) in if let err = err as? AWSMobileClientError { print("\(err.message)") @@ -91,6 +94,13 @@ class CognitoHelper { if let workHours = attributeMap["custom:work_hours"] { CognitoHelper.user?.setWorkHours(workHours: workHours) } + if let question = attributeMap["custom:securityquestion"] { + CognitoHelper.user?.setSecurityQuestion(securityQuestion: question) + } + if let answer = attributeMap["custom:securityanswer"] { + CognitoHelper.user?.setSecurityAnswer(securityAnswer: answer) + } + } func setHealthcareInformation(role:String, hospital:String, hospitalWebsite: String, healthcareProvider: String, healthcareWebsite:String, onDone: @escaping (_ success: Bool, _ errMessage: String)->Void) { diff --git a/DoctorsNote/DoctorsNote/ConnectionProcessor.swift b/DoctorsNote/DoctorsNote/ConnectionProcessor.swift index 74c37ec0..9d242e8d 100644 --- a/DoctorsNote/DoctorsNote/ConnectionProcessor.swift +++ b/DoctorsNote/DoctorsNote/ConnectionProcessor.swift @@ -80,8 +80,10 @@ class ConnectionProcessor { } } catch { + if String(data: data!, encoding: .utf8) == "null" { + return (nil, ConnectionError(message: "Null response")) + } return (nil, ConnectionError(message: "Malformed response body")) - } //print(type(of: jsonData!)) //print(jsonData!) @@ -162,11 +164,14 @@ class ConnectionProcessor { // let conversation = conversationList[conversationKey] as! [String : Any?] print(conversation["conversationID"] as? Int) print(conversation["converserID"] as? String) + print(conversation["converserPublicKey"] as? String) + print(conversation["adminPublicKey"] as? String) print(conversation["conversationName"] as? String) print(conversation["lastMessageTime"] as? TimeInterval) print(conversation["status"] as? Int) - if ((conversation["conversationID"] as? Int) != nil) && ((conversation["converserID"] as? String) != nil) && ((conversation["conversationName"] as? String) != nil) && ((conversation["lastMessageTime"] as? TimeInterval) != nil) && ((conversation["status"] as? Int) != nil) { - let newConversation = Conversation(conversationID: conversation["conversationID"] as! Int, converserID: conversation["converserID"] as! String, conversationName: conversation["conversationName"] as! String, lastMessageTime: Date(timeIntervalSince1970: (conversation["lastMessageTime"] as! TimeInterval) / 1000.0), status: conversation["status"] as! Int) + print((conversation["status"] as? Int) != nil) + if ((conversation["conversationID"] as? Int) != nil) && ((conversation["converserID"] as? String) != nil) && ((conversation["converserPublicKey"] as? String) != nil) && ((conversation["adminPublicKey"] as? String) != nil) && ((conversation["conversationName"] as? String) != nil) && ((conversation["lastMessageTime"] as? TimeInterval) != nil) && ((conversation["status"] as? Int) != nil) { + let newConversation = Conversation(conversationID: conversation["conversationID"] as! Int, converserID: conversation["converserID"] as! String, converserPublicKey: conversation["converserPublicKey"] as! String, adminPublicKey: conversation["adminPublicKey"] as! String, conversationName: conversation["conversationName"] as! String, lastMessageTime: Date(timeIntervalSince1970: (conversation["lastMessageTime"] as! TimeInterval) / 1000.0), status: conversation["status"] as! Int) conversations.append(newConversation) } else { return (nil, ConnectionError(message: "At least one JSON field was an incorrect format")) @@ -177,15 +182,15 @@ class ConnectionProcessor { func processUser(url: String, uid: String) -> (User?, ConnectionError?) { //Placeholder - return (User(uid: "-1", email: "email", firstName: "temp", middleName: "place", lastName: "holder", dateOfBirth: Date(), address: "nowhere", sex: "Male", phoneNumber: "9119119111", role: "Patient", healthSystems: [HealthSystem](), workHours: ""), nil) + return (User(uid: "-1", email: "email", firstName: "temp", middleName: "place", lastName: "holder", dateOfBirth: Date(), address: "nowhere", sex: "Male", phoneNumber: "9119119111", role: "Patient", healthSystems: [HealthSystem](), workHours: "", securityQuestion: "secQ", securityAnswer: "answer"), nil) } func processConversation(url: String, conversationID: Int) -> (Conversation?, ConnectionError?) { //Placeholder - return (Conversation(conversationID: -1, converserID: "-1", conversationName: "placeholder retrieval", lastMessageTime: Date(), status: -999), nil) + return (Conversation(conversationID: -1, converserID: "-1", converserPublicKey: "don't use me", adminPublicKey: "don't use me either", conversationName: "placeholder retrieval", lastMessageTime: Date(), status: -999), nil) } - func processMessages(url: String, conversationID: Int, numberToRetrieve: Int, startIndex: Int = 0, sinceWhen: Date = Date(timeIntervalSinceNow: TimeInterval(0))) throws -> [Message] { + func processMessages(url: String, conversationID: Int, numberToRetrieve: Int, cipher: MessageCipher? = nil, startIndex: Int = 0, sinceWhen: Date = Date(timeIntervalSinceNow: TimeInterval(0))) throws -> [Message] { var messageJSON = [String : Any]() messageJSON["conversationID"] = conversationID messageJSON["numberToRetrieve"] = numberToRetrieve @@ -212,8 +217,23 @@ class ConnectionProcessor { print((message["contentType"] as? Int) != nil) print((message["sender"] as? String) != nil) if ((message["messageId"] as? Int) != nil) && ((message["content"] as? String) != nil) && Data(base64Encoded: (message["content"] as! String)) != nil && ((message["contentType"] as? Int) != nil) && ((message["sender"] as? String) != nil) { + let messageBase64: String + if cipher != nil { + do { + let rawMessage = Data(base64Encoded: (message["content"] as! String))! + messageBase64 = try cipher!.decrypt(toDecrypt: rawMessage) + } catch let error as CipherError { + throw ConnectionError(message: error.getMessage()) + } + } else { + messageBase64 = (message["recieverContent"] as! String) + } + print(Data(base64Encoded: messageBase64) != nil) + if Data(base64Encoded: messageBase64) == nil { + throw ConnectionError(message: "Message JSON field did not meet encryption, encoding, and/or formatting requirements") + } let numFails = CognitoHelper.numFails - let newMessage = Message(messageID: message["messageId"] as! Int, conversationID: conversationID, content: Data(base64Encoded: (message["content"] as! String))!, contentType: message["contentType"] as! Int, sender: User(uid: message["sender"] as! String), numFails: numFails) + let newMessage = Message(messageID: message["messageId"] as! Int, conversationID: conversationID, content: Data(base64Encoded: messageBase64)!, contentType: message["contentType"] as! Int, sender: User(uid: message["sender"] as! String), numFails: numFails) messages.append(newMessage) } else { throw ConnectionError(message: "At least one JSON field was an incorrect format") @@ -224,12 +244,37 @@ class ConnectionProcessor { //TODO: Finer processing/passing of any errors returned by server to UI // - Need to discuss this with team - func processNewMessage(url: String, message: Message) -> ConnectionError? { + func processNewMessage(url: String, message: Message, cipher: MessageCipher? = nil, publicKeyExternalBase64: String? = nil, adminPublicKeyExternalBase64: String? = nil) -> ConnectionError? { var messageJSON = [String: Any]() messageJSON["conversationID"] = message.getConversationID() - messageJSON["content"] = message.getBase64Content() + if cipher != nil && publicKeyExternalBase64 != nil && adminPublicKeyExternalBase64 != nil { + var encryptedContent: Data + do { + encryptedContent = try cipher!.encrypt(toEncrypt: message.getBase64Content()) + } catch let error { + return ConnectionError(message: (error as! CipherError).getMessage()) + } + messageJSON["senderContent"] = encryptedContent.base64EncodedString() + do { + encryptedContent = try cipher!.encrypt(toEncrypt: message.getBase64Content(), publicKeyExternalBase64: publicKeyExternalBase64!) + } catch let error { + return ConnectionError(message: (error as! CipherError).getMessage()) + } + messageJSON["receiverContent"] = encryptedContent.base64EncodedString() + do { + encryptedContent = try cipher!.encrypt(toEncrypt: message.getBase64Content(), publicKeyExternalBase64: adminPublicKeyExternalBase64!) + } catch let error { + return ConnectionError(message: (error as! CipherError).getMessage()) + } + messageJSON["adminContent"] = encryptedContent.base64EncodedString() + } else { + messageJSON["senderContent"] = message.getBase64Content() + messageJSON["receiverContent"] = message.getBase64Content() + messageJSON["adminContent"] = message.getBase64Content() + } messageJSON["contentType"] = message.getContentType() messageJSON["numFails"] = message.getNumFails() + do { let data = try postData(urlString: url, dataJSON: messageJSON) if data.count != 0 { @@ -371,6 +416,32 @@ class ConnectionProcessor { } return appointments } + + //Returns the two encryptions of the private key (base64) data (in base64 format) + //Note: exact formatting may change and this method should make no assumptions nor do validation + func retrieveEncryptedPrivateKeys(url: String) throws -> (String, String) { + let emptyJSON = [String: Any]() + let data: [String : Any] + data = try postData(urlString: url, dataJSON: emptyJSON) + let keyJSON = data + if keyJSON["privateKeyP"] as? String == nil || keyJSON["privateKeyS"] as? String == nil { + throw ConnectionError(message: "At least one JSON field was missing or in an incorrect format") + } + return (keyJSON["privateKeyP"] as! String, keyJSON["privateKeyS"] as! String) + } + + func postKeys(url: String, privateKeyP: String , privateKeyS: String, publicKey: String) throws { + var keyJSON = [String: Any]() + keyJSON["privateKeyP"] = privateKeyP + keyJSON["privateKeyS"] = privateKeyS + keyJSON["publicKey"] = publicKey + + let data = try postData(urlString: url, dataJSON: keyJSON) + if data.count != 0 { + throw ConnectionError(message: "Non-blank return") + } + //Should have returned a blank 200 if successful, if so, no need to do anything + } } class Connector { diff --git a/DoctorsNote/DoctorsNote/Conversation.swift b/DoctorsNote/DoctorsNote/Conversation.swift index edb2ca0e..79b30eda 100644 --- a/DoctorsNote/DoctorsNote/Conversation.swift +++ b/DoctorsNote/DoctorsNote/Conversation.swift @@ -8,15 +8,19 @@ import Foundation class Conversation { - private let converserID: String private let conversationID: Int + private let converserID: String + private let converserPublicKey: String + private let adminPublicKey: String private var conversationName: String private var lastMessageTime: Date private var status: Int - init(conversationID: Int, converserID: String, conversationName: String, lastMessageTime: Date, status: Int) { + init(conversationID: Int, converserID: String, converserPublicKey: String, adminPublicKey: String, conversationName: String, lastMessageTime: Date, status: Int) { self.conversationID = conversationID self.converserID = converserID + self.converserPublicKey = converserPublicKey + self.adminPublicKey = adminPublicKey self.conversationName = conversationName self.lastMessageTime = lastMessageTime self.status = status @@ -28,11 +32,11 @@ class Conversation { let (potentialConversation, potentialError) = connectionProcessor.processConversation(url: ConnectionProcessor.standardUrl(), conversationID: conversationID) if (potentialError == nil && potentialConversation != nil) { let conversation = potentialConversation! - self.init (conversationID: conversationID, converserID: conversation.getConverserID(), conversationName: conversation.getConversationName(), lastMessageTime: conversation.getLastMessageTime(), status: conversation.getStatus()) + self.init (conversationID: conversationID, converserID: conversation.getConverserID(), converserPublicKey: conversation.getConverserPublicKey(), adminPublicKey: conversation.getAdminPublicKey(), conversationName: conversation.getConversationName(), lastMessageTime: conversation.getLastMessageTime(), status: conversation.getStatus()) } //Below this is a temporary mock for functionality let conversation = potentialConversation! - self.init (conversationID: conversationID, converserID: conversation.getConverserID(), conversationName: conversation.getConversationName(), lastMessageTime: conversation.getLastMessageTime(), status: conversation.getStatus()) + self.init (conversationID: conversationID, converserID: conversation.getConverserID(), converserPublicKey: conversation.getConverserPublicKey(), adminPublicKey: conversation.getAdminPublicKey(), conversationName: conversation.getConversationName(), lastMessageTime: conversation.getLastMessageTime(), status: conversation.getStatus()) //return nil } @@ -44,6 +48,14 @@ class Conversation { return converserID } + func getConverserPublicKey() -> String { + return converserPublicKey + } + + func getAdminPublicKey() -> String { + return adminPublicKey + } + func getConversationName() -> String { return conversationName } diff --git a/DoctorsNote/DoctorsNote/LocalCipher.swift b/DoctorsNote/DoctorsNote/LocalCipher.swift new file mode 100644 index 00000000..6a9bc9dd --- /dev/null +++ b/DoctorsNote/DoctorsNote/LocalCipher.swift @@ -0,0 +1,90 @@ +// +// LocalCipher.swift +// DoctorsNote +// +// Created by Merz on 4/12/20. +// Copyright © 2020 Team7. All rights reserved. +// + +import Foundation +import CryptoKit +import CommonCrypto + +class LocalCipher { + public func getAESFromPass(password: String, username: String) -> Data { + let newKey = UnsafeMutablePointer.allocate(capacity: 256) + CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, password.count, username, username.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 200000, newKey, 256) + let localAESKey = Data(bytes: newKey, count: 256) + return localAESKey + } + + private func getAESFromSecurityQuestions(securityQuestionAnswers: [String], username: String) -> Data { + let newKey = UnsafeMutablePointer.allocate(capacity: 256) + var saltValue = [UInt8]() + for char in username.cString(using: .utf8)! { + saltValue.append(UInt8(char)) + } + var combinedEntropy = username + for answer in securityQuestionAnswers { + combinedEntropy.append(answer) + } + CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), combinedEntropy, combinedEntropy.count, saltValue, username.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 200000, newKey, 256) + let localAESKey = Data(bytes: newKey, count: 256) + return localAESKey + } + + public func generateKetSet(password: String, securityQuestionAnswers: [String], username: String) -> (Data, Data, Data) { + let passwordAESKey = getAESFromPass(password: password, username: username) + let (privateKeyExport, publicKeyExport) = generateKeyPair() +// print("privateKeyExport: " + privateKeyExport.base64EncodedString()) + let passwordEncryptedPrivateKey = encryptKeyWithAES(keyExport: privateKeyExport, AESKey: passwordAESKey, iv: username) + let securityAESKey = getAESFromSecurityQuestions(securityQuestionAnswers: securityQuestionAnswers, username: username) + let secuiryQuestionEncryptedPrivateKey = encryptKeyWithAES(keyExport: privateKeyExport, AESKey: securityAESKey, iv: username) + return (passwordEncryptedPrivateKey, secuiryQuestionEncryptedPrivateKey, publicKeyExport) + } + + public func resetKeyPair(securityQuestionAnswers: [String], newPassword: String, username: String, connectionProcessor: ConnectionProcessor) throws { + let (_, encryptedPrivateKeySQ) = try connectionProcessor.retrieveEncryptedPrivateKeys(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/retrievekeys") + let AESKey = getAESFromSecurityQuestions(securityQuestionAnswers: securityQuestionAnswers, username: username) + let keyDecrypter = try MessageCipher(uniqueID: username, localAESKey: AESKey) + let (decryptedPrivateKey, publicKey) = try keyDecrypter.setAndReturnKeyPair(encryptedPrivateKey: encryptedPrivateKeySQ) + let encryptedPrivateKeyP = encryptKeyWithAES(keyExport: decryptedPrivateKey, AESKey: getAESFromPass(password: newPassword, username: username), iv: username) + try connectionProcessor.postKeys(url: "https://o2lufnhpee.execute-api.us-east-2.amazonaws.com/Development/addkeys", privateKeyP: encryptedPrivateKeyP.base64EncodedString(), privateKeyS: encryptedPrivateKeySQ, publicKey: publicKey.base64EncodedString()) + } + + private func generateKeyPair() -> (Data, Data) { + let privateKeyAttributes = [ kSecAttrKeyType: kSecAttrKeyTypeRSA, + kSecAttrKeySizeInBits: 2048, + kSecAttrKeyClass: kSecAttrKeyClassPrivate + ] as [CFString : Any] + let privateKey = SecKeyCreateRandomKey(privateKeyAttributes as CFDictionary, UnsafeMutablePointer?>.allocate(capacity: 100))! + var error: Unmanaged? + let cfExportPrivate = SecKeyCopyExternalRepresentation(privateKey, &error)! + let privateKeyExport = (cfExportPrivate as Data) + let publicKey = SecKeyCopyPublicKey(privateKey) + let cfExportPublic = SecKeyCopyExternalRepresentation(publicKey!, &error)! + let publicKeyExport = (cfExportPublic as Data) + return (privateKeyExport, publicKeyExport) + } + + private func encryptKeyWithAES(keyExport: Data, AESKey: Data, iv: String) -> Data { + //var toEncrypt = keyExport.base64EncodedString() + var toEncrypt = [UInt8](keyExport) + toEncrypt.reserveCapacity(toEncrypt.count / 128 * 128 + 128) + let encrypted = UnsafeMutablePointer.allocate(capacity: (toEncrypt.count / 128 * 128 + 128)) + var bytesEncrypted = 0 + let AESKeyUnsafe = UnsafeMutableBufferPointer.allocate(capacity: 256) + _ = AESKey.copyBytes(to: AESKeyUnsafe) +// print("Pre encryption") +// print(String(bytes: toEncrypt, encoding: .utf8)) + let encryptReturn = CCCrypt(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(), AESKeyUnsafe.baseAddress, kCCKeySizeAES256, iv, toEncrypt, toEncrypt.count / 128 * 128 + 128, encrypted, (toEncrypt.count / 128 * 128 + 128), &bytesEncrypted) + if encryptReturn != 0 { + print("Encryption for key failed with return: " + encryptReturn.description) + } + let encryptedKey = Data(bytes: encrypted, count: (toEncrypt.count / 128 * 128 + 128)) +// print("Post encryption and encoding:") +// print(encryptedKey.base64EncodedString()) + return encryptedKey + } + +} diff --git a/DoctorsNote/DoctorsNote/Login.storyboard b/DoctorsNote/DoctorsNote/Login.storyboard index b7c92ed8..1f8f9131 100644 --- a/DoctorsNote/DoctorsNote/Login.storyboard +++ b/DoctorsNote/DoctorsNote/Login.storyboard @@ -744,6 +744,28 @@ + + + + + + + + + + + + + @@ -752,6 +774,7 @@ + @@ -760,6 +783,7 @@ + @@ -780,21 +804,26 @@ + + + + + @@ -886,6 +915,8 @@ + + @@ -895,7 +926,7 @@ - + @@ -1064,7 +1095,7 @@ - + @@ -1083,7 +1114,7 @@ - + @@ -1139,7 +1170,7 @@ - + @@ -1157,8 +1188,27 @@ + + + + + + + + + +