Sorry to be yet another post about encryption, but I am struggling with setting up some truly strong encryption in my application. I currently have a basic setup for login that utilizes keychain and is based off of Tim Mitra's tutorial, which works wonderfully. However, I am uncomfortable about storing the account email / username in UserDefaults as it isn't particularly secure. Is there a better method that anyone can come up with? Additionally, I am working on utilizing Realm's built in encryption features, however I am unsure as to how I should properly store the key for said encryption given that it is of type Data. I also have heard that I should double encrypt the user's credentials using Secure Enclave and possibly utilize the same technique with Realm's key. Is there a guide that someone could point me to? How would you better optimize my code to be brutally secure? I have already set the application to check the device for Cydia and other signs of jailbreaking so as to avoid keychain data dumps and plan on checking any / all urls called too.
Here's my implementation of Keychain:
private func setupAccount()
{
let newAccountName = inputFields[0].text
let newPassword = inputFields[1].text
let hasLoginKey = UserDefaults.standard.bool(forKey: "hasSetup")
if !hasLoginKey {
UserDefaults.standard.setValue(inputFields[0].text, forKey: "username")
}
do {
// This is a new account, create a new keychain item with the account name.
let passwordItem = KeychainLIPassItem(service: KeychainConfiguration.serviceName,
account: newAccountName!,
accessGroup: KeychainConfiguration.accessGroup)
// Save the password for the new item.
try passwordItem.savePassword(newPassword!)
} catch {
fatalError("Error updating keychain - \(error)")
}
UserDefaults.standard.set(true, forKey: "hasSetup")
}
Here is what I currently have for Realm Encryption:
private func keyValue() -> Data
{
var key = Data(count: 64)
_ = key.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 64, bytes)
}
return key
}
private func securitySettings() -> Realm.Configuration
{
let key = keyValue()
let config = Realm.Configuration(encryptionKey: key)
return config
}
private func setupObject()
{
do {
let realm = try Realm(configuration: securitySettings())
let profile = UserProfile()
profile.firstName = firstName
profile.lastName = lastName
profile.dateOfBirth = dateOfBirth
profile.gender = gender
try! realm.write {
realm.add(profile)
}
} catch let error as NSError {
fatalError("Error opening realm: \(error)")
}
}
Thank you so much!
Related
I am currently working on a project with a multi user system. The user is able to create new profiles which are saved persistently using CoreData.
My problem is: Only one profile can be the active one at a single time, so I would like to get the ObjectID of the created profile and save it to UserDefaults.
Further I was thinking that as soon as I need the data of the active profile, I can simply get the ObjectID from UserDefaults and execute a READ - Request which only gives me back the result with that specific ObjectID.
My code so far for SAVING THE DATA:
// 1. Create new profile entry to the context.
let newProfile = Profiles(context: context)
newProfile.idProfileImage = idProfileImage
newProfile.timeCreated = Date()
newProfile.gender = gender
newProfile.name = name
newProfile.age = age
newProfile.weight = weight
// 2. Save the Object ID to User Defaults for "activeUser".
// ???????????????????
// ???????????????????
// 3. Try to save the new profile by saving the context to the persistent container.
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
My code so far for READING THE DATA
// 1. Creates an request that is just pulling all the data.
let request: NSFetchRequest<Profiles> = Profiles.fetchRequest()
// 2. Try to fetch the request, can throw an error.
do {
let result = try context.fetch(request)
} catch {
print("Error reading data \(error)")
}
As you can see, I haven't been able to implement Part 2 of the first code block. The new profile gets saved but the ObjectID isn't saved to UserDefaults.
Also Party 1 of the second code block is not the final goal. The request just gives you back all the data of that entity, not only the one with the ObjectID I stored in User Defaults.
I hope you guys have an idea on how to solve this problem.
Thanks for your help in advance guys!
Since NSManagedObjectID does not conform to one of the types handled by UserDefaults, you'll have to use another way to represent the object id. Luckily, NSManagedObjectID has a uriRepresentation() that returns a URL, which can be stored in UserDefaults.
Assuming you are using a NSPersistentContainer, here's an extension that will handle the storage and retrieval of a active user Profile:
extension NSPersistentContainer {
private var managedObjectIDKey: String {
return "ActiveUserObjectID"
}
var activeUser: Profile? {
get {
guard let url = UserDefaults.standard.url(forKey: managedObjectIDKey) else {
return nil
}
guard let managedObjectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else {
return nil
}
return viewContext.object(with: managedObjectID) as? Profile
}
set {
guard let newValue = newValue else {
UserDefaults.standard.removeObject(forKey: managedObjectIDKey)
return
}
UserDefaults.standard.set(newValue.objectID.uriRepresentation(), forKey: managedObjectIDKey)
}
}
}
This uses a method on NSPersistentStoreCoordinator to construct a NSManagedObjectID from a URI representation.
I'm dealing with the scenario, where a user has previously deleted the app and has now re-installed it.
It was hitting my delta fetch function, which is receiving a lot of old subscription notifications, mostly deletes. But not downloading current records.
I'm now adding code to perform a fetch on each record type to download all the data.
I'd like to reset delta fetch server token, so the app doesn't have to process old subscriptions notifications. However I can't find how to do this, maybe it's not possible.
Are you referring to CKServerChangeToken (documentation) when you say "delta fetch server token"? And are you attempting to sync within the CloudKit private database?
Assuming that is true, here is an example of how I fetch changes from the private database and keep track of the sync token:
//MARK: Fetch Latest from CloudKit from private DB
func fetchPrivateCloudKitChanges(){
print("Fetching private changes...")
//:::
let privateZoneId = CKRecordZone.ID(zoneName: CloudKit.zoneName, ownerName: CKCurrentUserDefaultName)
/----
let options = CKFetchRecordZoneChangesOperation.ZoneOptions()
options.previousServerChangeToken = previousChangeToken
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [privateZoneId], optionsByRecordZoneID: [recordZoneID:options])
//Queue up the updated records to process below
var records = [CKRecord]()
operation.recordChangedBlock = { record in
records.append(record)
}
operation.recordWithIDWasDeletedBlock = { recordId, type in
//Process a deleted record in your local database...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
// Save new zone change token to disk
previousChangeToken = token
}
operation.recordZoneFetchCompletionBlock = { zoneId, token, _, _, error in
if let error = error {
print(error)
}
// Write this new zone change token to disk
previousChangeToken = token
}
operation.fetchRecordZoneChangesCompletionBlock = { error in
if let error = error {
print(error
}else{
//Success! Process all downloaded records from `records` array above...
//records...
}
}
CloudKit.privateDB.add(operation)
}
//Change token property that gets saved and retrieved from UserDefaults
var previousChangeToken: CKServerChangeToken? {
get {
guard let tokenData = defaults.object(forKey: "previousChangeToken") as? Data else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? CKServerChangeToken
}
set {
guard let newValue = newValue else {
defaults.removeObject(forKey: "previousChangeToken")
return
}
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
defaults.set(data, forKey: "previousChangeToken")
}
}
Your specific situation might differ a little, but I think this is how it's generally supposed to work when it comes to staying in sync with CloudKit.
Update
You could try storing the previousServerChangeToken on the Users record in CloudKit (you would have to add it as a new field). Each time the previousServerChangeToken changes in recordZoneFetchCompletionBlock you would have to save it back to iCloud on the user's record.
Suppose John developed App A and Heather developed App B. They each have different Apple Developer's accounts and they are not on the same team or associated in any way. App B is backed by a public CloudKit database. Is there any way for App A to write to App B's public CloudKit database? Namely, can App A do this:
let DB = CKContainer(identifier: "iCloud.com.Heather.AppB").publicCloudDatabase
and then write to or read from this DB?
I'm guessing that this is not allowed out of the box, but is there a way to set up authentication so that this is possible?
This looks/sounds like the solution you seek.
CloudKit share Data between different iCloud accounts but not with everyone as outlined by https://stackoverflow.com/users/1878264/edwin-vermeer an iCloud specialist on SO.
There is third party explaination on this link too. https://medium.com/#kwylez/cloudkit-sharing-series-intro-4fc82dad7a9
Key steps shamelessly cut'n'pasted ... make sure you read and credit Cory on medium.com!
// Add an Info.plist key for CloudKit Sharing
<key>CKSharingSupported</key>
<true/>
More code...
CKContainer.default().discoverUserIdentity(withPhoneNumber: phone, completionHandler: {identity, error in
guard let userIdentity: CKUserIdentity = identity, error == nil else {
DispatchQueue.main.async(execute: {
print("fetch user by phone error " + error!.localizedDescription)
})
return
}
DispatchQueue.main.async(execute: {
print("user identity was discovered \(identity)")
})
})
/// Create a shred the root record
let recordZone: CKRecordZone = CKRecordZone(zoneName: "FriendZone")
let rootRecord: CKRecord = CKRecord(recordType: "Note", zoneID: recordZone.zoneID)
// Create a CloudKit share record
let share = CKShare(rootRecord: rootRecord)
share[CKShareTitleKey] = "Shopping List” as CKRecordValue
share[CKShareThumbnailImageDataKey] = shoppingListThumbnail as CKRecordValue
share[CKShareTypeKey] = "com.yourcompany.name" as CKRecordValue
/// Setup the participants for the share (take the CKUserIdentityLookupInfo from the identity you fetched)
let fetchParticipantsOperation: CKFetchShareParticipantsOperation = CKFetchShareParticipantsOperation(userIdentityLookupInfos: [userIdentity])
fetchParticipantsOperation.fetchShareParticipantsCompletionBlock = {error in
if let error = error {
print("error for completion" + error!.localizedDescription)
}
}
fetchParticipantsOperation.shareParticipantFetchedBlock = {participant in
print("participant \(participant)")
/// 1
participant.permission = .readWrite
/// 2
share.addParticipant(participant)
let modifyOperation: CKModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [rootRecord, share], recordIDsToDelete: nil)
modifyOperation.savePolicy = .ifServerRecordUnchanged
modifyOperation.perRecordCompletionBlock = {record, error in
print("record completion \(record) and \(error)")
}
modifyOperation.modifyRecordsCompletionBlock = {records, recordIDs, error in
guard let ckrecords: [CKRecord] = records, let record: CKRecord = ckrecords.first, error == nil else {
print("error in modifying the records " + error!.localizedDescription)
return
}
/// 3
print("share url \(url)")
}
CKContainer.default().privateDB.add(modifyOperation)
}
CKContainer.default().add(fetchParticipantsOperation)
And on the other side of the fence.
let acceptShareOperation: CKAcceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [shareMeta])
acceptShareOperation.qualityOfService = .userInteractive
acceptShareOperation.perShareCompletionBlock = {meta, share, error in
Log.print("meta \(meta) share \(share) error \(error)")
}
acceptShareOperation.acceptSharesCompletionBlock = {error in
Log.print("error in accept share completion \(error)")
/// Send your user to wear that need to go in your app
}
CKContainer.default().container.add(acceptShareOperation)
Really I cannot hope to do the article justice, go read it... its in three parts!
If the apps were in the same organization, there is a way to set up shared access. But as you described the situation, it is not possible.
I have a system where each user allowed to install apps for two devices only. The issue is happened when user uninstall and reinstall again on the same device. So it will generate new UUID and when the apps check to web service.
The apps will send UUID and login id in order to check if the user with that login id has installed in more than two devices. I am using real iPhone and iPad device, not using simulator. I am not sure for production environment. Currently the apps is distributed using Apple TestFlight using AppStore Distribution profile.
I generate the uuid using this
let uuid = UIDevice.currentDevice().identifierForVendor!.UUIDString
Thanks.
You need to use keychain: (to be able to imort JNKeychain you need to enter a new string in your pod file pod 'JNKeychain'). This will guaranty you that if you don't change you bundle identifier, you will always have a uniq device id (that will stay the same even after deleting your app). I used that when user was banned forever in our application, he couldn't enter the app even with different account even after deleting the app.
import UIKit
import JNKeychain
class KeychainManager: NSObject {
static let sharedInstance = KeychainManager()
func getDeviceIdentifierFromKeychain() -> String {
// try to get value from keychain
var deviceUDID = self.keychain_valueForKey("keychainDeviceUDID") as? String
if deviceUDID == nil {
deviceUDID = UIDevice.current.identifierForVendor!.uuidString
// save new value in keychain
self.keychain_setObject(deviceUDID! as AnyObject, forKey: "keychainDeviceUDID")
}
return deviceUDID!
}
// MARK: - Keychain
func keychain_setObject(_ object: AnyObject, forKey: String) {
let result = JNKeychain.saveValue(object, forKey: forKey)
if !result {
print("keychain saving: smth went wrong")
}
}
func keychain_deleteObjectForKey(_ key: String) -> Bool {
let result = JNKeychain.deleteValue(forKey: key)
return result
}
func keychain_valueForKey(_ key: String) -> AnyObject? {
let value = JNKeychain.loadValue(forKey: key)
return value as AnyObject?
}
}
This is the expected bevaiour. If you want to use same UUID you need to save it to the keyChain. I have done something simmiler in of my apps using KeyChainWrapper
So here is a sample chunk for you
let deviceId = UIDevice.currentDevice().identifierForVendor?.UUIDString ?? ""
// Saving Id in keyChain
KeychainWrapper.defaultKeychainWrapper().setString(deviceId, forKey: "CurrentDeviceId")
And then just get Id from keyChain everytime you want to use it.
let previousDeviceId = KeychainWrapper.defaultKeychainWrapper().stringForKey("CurrentDeviceId")
I'm developing an iOS Application that uses Google Endpoints API. In order to authorise the requests, the user must sign in with his Gmail account on the first screen. I've managed to get this to work but the problem is that the user has to log in every single time he launches the app. Is there a way to have the session last a bit longer? For example, when using the Facebook SDK for iOS, once the user logs in with Facebook, the session stays active until the user explicitly logs out.
Thanks,
I've come up with what I consider to be a hack that solves this problem.
Step 1: After the user signs in with his/her Gmail account, save the authentication object properties to NSUserDefaults.
#objc(viewController:finishedWithAuth:error:)
func finishedWithAuth(viewController :GTMOAuth2ViewControllerTouch , auth:GTMOAuth2Authentication,error:NSError!){
self.dismissViewControllerAnimated(true, completion: nil);
if error != nil {
println("Authentication Failure: \(error.localizedDescription)");
}else{
GoogleEndpointAssistant.saveGTMOAuth2AuthenticationToUserDefaults(auth)
setAuthentication(auth)
}
GoogleEndpointAssistant.swift
class func saveGTMOAuth2AuthenticationToUserDefaults(auth : GTMOAuth2Authentication!){
assert(auth.parameters != nil)
assert(auth.parameters.count > 0)
auth.parameters.setValue(auth.tokenURL.absoluteString, forKey: "token_url")
auth.parameters.setValue(auth.redirectURI, forKey: "redirect_url")
let defaults = NSUserDefaults.standardUserDefaults();
defaults.setObject(auth.parameters, forKey: GMTOAuth2AuthenticationKey)
}
Step 2: Rebuild the auth object in viewDidLoad:
override func viewDidLoad() {
if let auth : GTMOAuth2Authentication = GoogleEndpointAssistant.rebuildGTMOAuth2AuthenticationFromUserDefaults(){
setAuthentication(auth)
}
}
GoogleEndpointAssistant.swift
class func rebuildGTMOAuth2AuthenticationFromUserDefaults()->GTMOAuth2Authentication?{
let defaults = NSUserDefaults.standardUserDefaults();
if let parameters = defaults.dictionaryForKey(GMTOAuth2AuthenticationKey){
let serviceProvider = parameters["serviceProvider"] as? String
let tokenURL = NSURL(string:parameters["token_url"] as String)
let redirectURL = parameters["redirect_url"] as String
let auth : GTMOAuth2Authentication = GTMOAuth2Authentication.authenticationWithServiceProvider(
serviceProvider,
tokenURL: tokenURL,
redirectURI: redirectURL,
clientID: GoogleCloudEndPointClientID,
clientSecret: GoogleCloudEndPointClientSecret) as GTMOAuth2Authentication
auth.userEmail = parameters["email"] as String
auth.userID = parameters["userID"] as String
auth.userEmailIsVerified = parameters["isVerified"] as String
auth.scope = parameters["scope"] as String
auth.code = parameters["code"] as String
auth.tokenType = parameters["token_type"] as String
auth.expiresIn = (parameters["expires_in"] as NSNumber).longValue
auth.refreshToken = parameters["refresh_token"] as String
auth.accessToken = parameters["access_token"] as String
return auth
}
return nil
}