Using iOS Notifications, Cryptography and iCloud to build your own Chat App V

I should start this article with a disclaimer, it based on iOS 13, Swift 5 and Xcode 11.x. If you reading this and those numbers look dated, be forewarned.

I should also warn you that notifications and iCloud code, involve Apple’s infrastructure which means you will need an Apple Developers account to use them.

Obviously this is part V, you need to go back to the beginning for it to make any sense, indeed you need to look at the notifications series before do that else your struggle, yes I am sorry, but we’re talking about 10 medium articles with an average 4 minute read each, so 40 minutes before you get here. 40 minutes that is not including cut’n’paste and debugging time :) but hey wait … you’ll have kick a**e chat app by that point. Imagine in less an hour you could have built your own alternative to whatapp.

Ok, hopefully that preamble didn’t completely put you off and you’re still here. Before I continue coding, some open thoughts.

We left off at a crash site. Did you figure out what the bug was. It was the sequence. If you choose a person to message before they had logged into another device, it picked up the wrong public signature, an old one which at worst crashed the app, at best left you in dark aka it didn’t work.

Now we can fix the crash, but the problem remains. One of you has an old signature and even worst you sent an invite using it. What is the fix.

Fix.1

My first thought was to freeze the public/private key pair generation. You do it once, never replace it again. I think it will work, but we can surely do better.

Fix 2.

At ground zero, there is no public key to use, so it impossible to use the wrong one. But that also means you cannot talk to anybody, so it is a bit of an awkward start. Assuming you get your friend to login, you can chat, but wait the risk of the wrong sequence then comes into play. We could delete all public keys when we finished talking/closed the app, but that leaves us at ground zero again. Forcing the sync dance via some other means every time we use it. We got to do better.

Fix 3.

We could use some sort timestamp, maybe the public key already contains one. But does it really help us? If they’re not already online, they will invalidate it as soon as the sign in. Is this a dead end.

Fix 4.

A handshake might be a solution. We could use silent notifications to confirm a connection has been established. If we don’t get a confirmation they got our initial ping, we resign again and try again. I think that would work.

Fix 5.

We could change our database and include a history of public signatures. When we sign in, we add an additional signature. If we are unable to decrypt a token, we try an older public key. It might work, but it raises quite a few more questions. How many signatures would we store, for how long, how long would it take to cycle thru these beasts. It might work, but I don’t know how scalable it is ultimately.

I looked at the code, Fix 4. a handshake seemed like the best choice and tried to put some theory in practice, but I didn’t make it work.

I looked at the code again, Fix 5. might work, I spent half a day working thru the code, but I couldn’t make it work 100%. I could now logout and log back in without having to reset everything. But I needed to make sure I used the correct sequence, a sequence that would be impossible in the real world.

Let me try and restate the problem. The issue was that although I could detect the absence of a signature; I could not tell if it had been superseded by a new one, beyond the fact that if I tried to decrypt it with a signature it would fail to decrypt. Worst I could not signal the other party I had failed, the action they needed to take place on the other side. I was nervous of the iCloud dance that was taking place too, it had too many places to fall over.

Fix 6.

I wasn’t keep on the idea, but I could store the tokens I had successfully decoded in my private database. A list I would use as a backup. I would remove the iCloud dance, and simple delete records in the mediator database that failed too decrypt.

Fix 7.

I store a key that was just for tokens within the app itself, an unchanging encryption key. It was the same as Fix 1. Only it wasn’t for an individual, it was for all tokens/an address. I would/could then use the encyption tech on the messages themselves. Yes I was changing the rules, but the project was starting to teeter on the edge of a black hole.

Fix 8.

I would store the tokens in clear text. But in order to use one to send a message you would need to ask permission [thru sending a message of course]. So your first communication would be a tightly controlled exchange of pseudo keys of sort. This sounded feasible. Needed to think it through some more, but maybe this was/is the middle kingdom I was trying to find.

But stop, enough text. I need to bring this article to an end. I didn’t want to leave without giving you some code, so here is Fix 5. or sorts. You need the correct sequence each time, but unlike its predecessor it will work again and again. We need to add a monster code block to our cloudKit.

func getPrivateK(name: String) {
let predicate = NSPredicate(format: "name = %@", name)
let query = CKQuery(recordType: "directory", predicate: predicate)
privateDB.perform(query,
inZoneWith: CKRecordZone.default().zoneID) { [weak self] results, error in
guard let _ = self else { return }
if let error = error {
DispatchQueue.main.async {
print("error",error)
}
return
}
guard let results = results else { return }
for result in results {
let privateK = result.object(forKey: "privateK") as? Data
rsa.putPrivateKey(privateK: privateK!, keySize: 2048, privateTag: "ch.cqd.noob", publicTag: "ch.cqd.noob")
self!.getPublicK(name: name)
}
if results.count == 0 {
DispatchQueue.main.async {
messagePublisher.send(name + " No Private Key")
}
let success = rsa.generateKeyPair(keySize: 2048, privateTag: "ch.cqd.noob", publicTag: "ch.cqd.noob")
if success {
let privateK = rsa.getPrivateKey()
let publicK = rsa.getPublicKey()
self!.searchAndUpdate(name: name, publicK: publicK!, privateK: privateK!)
}
return
}
}
return
}
func getPublicK(name: String) {
let predicate = NSPredicate(format: "name = %@", name)
let query = CKQuery(recordType: "directory", predicate: predicate)
publicDB.perform(query,inZoneWith: CKRecordZone.default().zoneID) { [weak self] results, error in
guard let _ = self else { return }
if let error = error {
DispatchQueue.main.async {
print("error",error)
}
return
}
guard let results = results else { return }
for result in results {let publicK = result.object(forKey: "publicK") as? Data
rsa.putPublicKey(publicK: publicK!, keySize: 2048, privateTag: "ch.cqd.noob", publicTag: "ch.cqd.noob")
self!.getTokens(name: name)
}
if results.count == 0 {
print("no name ",name)
DispatchQueue.main.async {
messagePublisher.send(name + " No Public Key")
}
return
}
}
return
}
func getTokens(name: String) {
var mediator:[CKRecord] = []
let predicate = NSPredicate(format: "name = %@", name)
let query = CKQuery(recordType: "mediator", predicate: predicate)
publicDB.perform(query,inZoneWith: CKRecordZone.default().zoneID) { [weak self] results, error in
guard let _ = self else { return }
if let error = error {
DispatchQueue.main.async {
print("error",error)
}
return
}
guard let results = results else {
DispatchQueue.main.async {
messagePublisher.send(name + "No tokens to change")
}
return
}
for result in results {
let token = rsa.decprypt(encrpted: result.object(forKey: "token") as! [UInt8])
if token != nil {
result.setObject(token as CKRecordValue?, forKey: "senderDevice")
mediator.append(result)
} else {
self!.publicDB.delete(withRecordID: result.recordID) { (recordID, error) in
// move on
}
}
}
if results.count > 0 {
self!.resetSignature(tokens2Change:mediator,name: name)
}
}
}
private func resetSignature(tokens2Change:[CKRecord],name:String) {
let success = rsa.generateKeyPair(keySize: 2048, privateTag: "ch.cqd.noob", publicTag: "ch.cqd.noob")
if success {
for review in tokens2Change {
let token = review.object(forKey: "senderDevice") as? String
let encryptedToken = rsa.encrypt(text: token!)
review.setObject(encryptedToken as CKRecordValue?, forKey: "token")
}
let saveRecordsOperation = CKModifyRecordsOperation()
saveRecordsOperation.recordsToSave = tokens2Change
saveRecordsOperation.savePolicy = .allKeys
saveRecordsOperation.modifyRecordsCompletionBlock = { savedRecords,deletedRecordID, error in
if error != nil {
print("error")
} else {
// print("saved ",savedRecords?.count)
}
}
publicDB.add(saveRecordsOperation)
let privateK = rsa.getPrivateKey()
let publicK = rsa.getPublicKey()
cloud.searchAndUpdate(name: name, publicK: publicK!, privateK: privateK!)
}
}

We have with this version introduced a privateDB copy of the last signature used. And I had add this method to our RSA.swift code too.

func putPrivateKey(privateK:Data, keySize: UInt, privateTag: String, publicTag: String) {
let attributes: [String:Any] = [
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: keySize,
kSecAttrIsPermanent as String: true as AnyObject,
kSecAttrApplicationTag as String: privateTag as AnyObject
]
self.privateKey = SecKeyCreateWithData(privateK as CFData, attributes as CFDictionary, nil)
print("putprivatekey ",self.privateKey)
}

Finally the changes to ContentView.swift to make use of the whole thing. Be careful how you code this cause I am going mothball the whole thing in the next article. Firstly the upperwheel code changes

.onTapGesture {
self.sender = self.users[self.selected]
cloud.getPrivateK(name: self.sender)
messagePublisher.send(self.sender + " Logged In")
self.disableUpperWheel = true
}.disabled(disableUpperWheel)

Which I think is all the changes you need to make. If you’re following along than let me know in the comments if I missed anything. Read on to see what we do next.

Coding for 35+ years, enjoying using and learning Swift/iOS development. Writer @ Better Programming, @The StartUp, @Mac O’Clock, Level Up Coding & More

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store