How to get the realy fixed Device-ID in swift?

user10118085 picture user10118085 · Aug 1, 2018 · Viewed 10.4k times · Source

I use the below code since long time. I have thought it is unique

But I have deleted my app and reinstalled it, I get new different Device-ID.

if let uuid = UIDevice.current.identifierForVendor?.uuidString {
  print(uuid)
}

every new reinstall, I get a new ID.

How do I get something which stays the same?

Answer

digitalHound picture digitalHound · Aug 2, 2018

Since the value returned from identifierForVendor can be cleared when deleting the app or reset if the user resets it in the Settings app, you have to manage persisting it yourself.

There are a few ways to accomplish this. You can setup a server that assigns a uuid which is then persisted and fetched server side via user login, or you can create and store it locally in the keychain.

Items stored in the keychain will not be deleted when the app is deleted. This allows you to check if a uuid was previously stored, if so you can retrieve it, if not you can generate a new uuid and persist it.

Here's a way you could do it locally:

/// Creates a new unique user identifier or retrieves the last one created
func getUUID() -> String? {

    // create a keychain helper instance
    let keychain = KeychainAccess()

    // this is the key we'll use to store the uuid in the keychain
    let uuidKey = "com.myorg.myappid.unique_uuid"

    // check if we already have a uuid stored, if so return it
    if let uuid = try? keychain.queryKeychainData(itemKey: uuidKey), uuid != nil {
        return uuid
    }

    // generate a new id
    guard let newId = UIDevice.current.identifierForVendor?.uuidString else {
        return nil
    }

    // store new identifier in keychain
    try? keychain.addKeychainData(itemKey: uuidKey, itemValue: newId)

    // return new id
    return newId
}

And here's the class for storing/retrieving from the keychain:

import Foundation

class KeychainAccess {

    func addKeychainData(itemKey: String, itemValue: String) throws {
        guard let valueData = itemValue.data(using: .utf8) else {
            print("Keychain: Unable to store data, invalid input - key: \(itemKey), value: \(itemValue)")
            return
        }

        //delete old value if stored first
        do {
            try deleteKeychainData(itemKey: itemKey)
        } catch {
            print("Keychain: nothing to delete...")
        }

        let queryAdd: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemKey as AnyObject,
            kSecValueData as String: valueData as AnyObject,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]
        let resultCode: OSStatus = SecItemAdd(queryAdd as CFDictionary, nil)

        if resultCode != 0 {
            print("Keychain: value not added - Error: \(resultCode)")
        } else {
            print("Keychain: value added successfully")
        }
    }

    func deleteKeychainData(itemKey: String) throws {
        let queryDelete: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemKey as AnyObject
        ]

        let resultCodeDelete = SecItemDelete(queryDelete as CFDictionary)

        if resultCodeDelete != 0 {
            print("Keychain: unable to delete from keychain: \(resultCodeDelete)")
        } else {
            print("Keychain: successfully deleted item")
        }
    }

    func queryKeychainData (itemKey: String) throws -> String? {
        let queryLoad: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: itemKey as AnyObject,
            kSecReturnData as String: kCFBooleanTrue,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let resultCodeLoad = withUnsafeMutablePointer(to: &result) {
            SecItemCopyMatching(queryLoad as CFDictionary, UnsafeMutablePointer($0))
        }

        if resultCodeLoad != 0 {
            print("Keychain: unable to load data - \(resultCodeLoad)")
            return nil
        }

        guard let resultVal = result as? NSData, let keyValue = NSString(data: resultVal as Data, encoding: String.Encoding.utf8.rawValue) as String? else {
            print("Keychain: error parsing keychain result - \(resultCodeLoad)")
            return nil
        }
        return keyValue
    }
}

Then you can just have a user class where you get the identifier:

let uuid = getUUID()
print("UUID: \(uuid)") 

If you put this in a test app in viewDidLoad, launch the app and note the uuid printed in the console, delete the app and relaunch and you'll have the same uuid.

You can also create your own completely custom uuid in the app if you like by doing something like this:

// convenience extension for creating an MD5 hash from a string
extension String {

    func MD5() -> Data? {
        guard let messageData = data(using: .utf8) else { return nil }

        var digestData = Data(count: Int(CC_MD5_DIGEST_LENGTH))
        _ = digestData.withUnsafeMutableBytes { digestBytes in
            messageData.withUnsafeBytes { messageBytes in
                CC_MD5(messageBytes, CC_LONG(messageData.count), digestBytes)
            }
        }

        return digestData
    }
}

// extension on UUID to generate your own custom UUID
extension UUID {

    static func custom() -> String? {
        guard let bundleID = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String else {
            return nil
        }

        let unique = bundleID + NSUUID().uuidString
        let hashData = unique.MD5()
        let md5String = hashData?.map { String(format: "%02hhx", $0) }.joined()

        return md5String
    }
}

Note that to use the MD5 function you will have to add the following import to an Objective-C bridging header in your app: (if you're building with Xcode < 10. In Xcode 10+ CommonCrypto is included so you can skip this step)

#import <CommonCrypto/CommonCrypto.h>

If your app does not have a bridging header, add one to your project and make sure to set it in build settings:

Screenshot

Once it's setup you can generate your own custom uuid like this:

let otherUuid = UUID.custom()
print("Other: \(otherUuid)")

Running the app and logging both outputs generates uuids something like this:

// uuid from first example
UUID: Optional("8A2496F0-EFD0-4723-8C6D-8E18431A49D2")

// uuid from second custom example
Other: Optional("63674d91f08ec3aaa710f3448dd87818")