
E2EE messenger for iOS
Swift Go/Golang Networking EncryptionTask
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!