LevelBuilder

E2EE messenger for iOS

Swift Go/Golang Networking Encryption

Task

Though there are many messaging apps available online, many do not offer a satisfactory level of security and privacy, or require the user to trust the application provider. Though there are some platforms such as Signal and WhatsApp, which offer true E2E encryption, they do so using asymmetric encryption, meaning that they in theory could be broken, and they require a phone number to be linked to an account, deanonymizing the user at best. As such I decided to create an alternative that is both user and privacy/security centric, where the user does not need to trust any actor in the information flow, including the ISP provider and platform server manager/provider.

Solution

In order to truly not need to trust anyone in the data flow from one device to another, the best method is to use a symmetric key encryption such as AES (the system uses AES-GCM), which has to be shared between two devices in a secure way, preferably in person. This way, in theory, the only ones capable of decrypting a message would be the two devices with the key, that can be periodically changed. I have thus far created a messaging app for iOS using SwiftUI and Go for the backend server. I created a NotificationServiceExtension to decrypt messages in the background when they are sent as Push Notifications (when the device is not connected to the Go server). Now I am focusing on implementing an audio and video call option using WebRTC and CallKit.

The project is not publicly available as it is a work in progress, but some code snippets from the project are available below:


extension String {
    func isValidEmail() -> Bool {
        let regex = try! NSRegularExpression(pattern: "^[a-zA-Z0-9.!#$%&’*+/=?^_{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$", options: .caseInsensitive)
        return regex.firstMatch(in: self, range: NSRange(location: 0, length: count)) != nil
    }
    func isValidUsername() -> Bool {
        let regex = try! NSRegularExpression(pattern: "[a-zA-Z0-9]")
        return regex.firstMatch(in: self, range: NSRange(location: 0, length: count)) != nil
    }
    func isValidPassword() -> Bool {
        let regex = try! NSRegularExpression(pattern: "(?=.{8,})")
        return regex.firstMatch(in: self, range: NSRange(location: 0, length: count)) != nil
    }
}
    

private func addContact() {
    let newActiveKey = SymmetricKey(size: .bits256).withUnsafeBytes{
        return Data(Array($0)).base64EncodedString()
    }
    //Check if exists
    let fetchRequest: NSFetchRequest<Contact>
    fetchRequest = Contact.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "nickname == '(newContact)'")
    var existantUsers: [Contact]
    do {
        existantUsers = try self.viewContext.fetch(fetchRequest)
    } catch { return }
    if existantUsers.count == 0 {
        //Create new
        let newContactO = Contact(context: viewContext)
        newContactO.user = newContact
        newContactO.nickname = newContact
        newContactO.timestamp = Date()
        newContactO.group = false
        newContactO.sharedKey = sharedKey
        newContactO.activeKey = newActiveKey
        newContactO.oldActiveKey = ""
        newContactO.icon = ""
        newContactO.iconData = Data()
        newContactO.descriptionText = ""
        newContactO.newMessage = false
        newContactO.messageTimestamp = Date()
        newContactO.lastSeenTimestamp = Date()
        newContactO.latestMessage = ""
        newContactO.secure = true
    } else {
        //Refresh
        let refreshingContact = existantUsers.first
        refreshingContact?.user = newContact
        refreshingContact?.nickname = newContact
        refreshingContact?.timestamp = Date()
        refreshingContact?.group = false
        refreshingContact?.sharedKey = sharedKey
        refreshingContact?.activeKey = newActiveKey
        refreshingContact?.secure = true
    }
    do {
        try viewContext.save()
    } catch {
        let nsError = error as NSError
        fatalError("Unresolved error (nsError), (nsError.userInfo)")
    }
    model.sendMessage(contact: newContact, group: false, receiver: newContact, hash: SymmetricKey(size: .bits256).withUnsafeBytes{ return Data(Array($0)).base64EncodedString() }, key: sharedKey, text: "NEW_CONTACT:(newActiveKey):(model.profilePicture)")
    newContact = ""
    sharedKey = ""
}
    

ForEach(messages) { message in
    let minutesAgo = Int(-message.timestamp!.timeIntervalSinceNow)/60
    let failedToSend = (Int(message.status) == 0 && minutesAgo > 5) || (message.notReceivedBy != "" && minutesAgo > 10080)
    let bySelf = model.username == message.by
    let illegible = message.text!.starts(with: "<ILLEGIBLE>") || message.reaction!.contains("<UnencryptedRequest>")
    MessageBubbleView(bySelf: bySelf, by: message.by!, encrypted: message.encrypted, group: group, failedToSend: failedToSend, timestamp: message.timestamp!, status: Int(message.status), text: message.text!, metadata: message.metadata ?? Data(), reaction: message.reaction!)
    

Demo

A demo video of the project is available below. Please feel free to take a look!