├── .gitignore ├── SecureUDIDTests ├── en.lproj │ └── InfoPlist.strings ├── SecureUDIDTests-Info.plist └── SecureUDIDTests.m ├── SecureUDID-Prefix.pch ├── SecureUDID.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── SecureUDID.xcscheme └── project.pbxproj ├── SecureUDID.h ├── README.md └── SecureUDID.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | *.xcuserstate 4 | *.xcuserdatad 5 | 6 | -------------------------------------------------------------------------------- /SecureUDIDTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /SecureUDID-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'SecureUDID' target in the 'SecureUDID' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /SecureUDID.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SecureUDIDTests/SecureUDIDTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | org.secureudid.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SecureUDID.xcodeproj/xcshareddata/xcschemes/SecureUDID.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 14 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 32 | 38 | 39 | 40 | 41 | 42 | 51 | 52 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /SecureUDID.h: -------------------------------------------------------------------------------- 1 | // 2 | // SecureUDID.h 3 | // SecureUDID 4 | // 5 | // Created by Crashlytics Team on 3/22/12. 6 | // Copyright (c) 2012 Crashlytics, Inc. All rights reserved. 7 | // http://www.crashlytics.com 8 | // info@crashlytics.com 9 | // 10 | 11 | /* 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | this software and associated documentation files (the "Software"), to deal in 14 | the Software without restriction, including without limitation the rights to 15 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 16 | of the Software, and to permit persons to whom the Software is furnished to do 17 | so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | 31 | #import 32 | 33 | @interface SecureUDID : NSObject 34 | 35 | /* 36 | Returns a unique id for the device, sandboxed to the domain and salt provided. This is a potentially 37 | expensive call. You should not do this on the main thread, especially during launch. 38 | 39 | retrieveUDIDForDomain:usingKey:completion: is provided as an alternative for your 4.0+ coding convenience. 40 | 41 | Example usage: 42 | #import "SecureUDID.h" 43 | 44 | NSString *udid = [SecureUDID UDIDForDomain:@"com.example.myapp" key:@"difficult-to-guess-key"]; 45 | 46 | */ 47 | + (NSString *)UDIDForDomain:(NSString *)domain usingKey:(NSString *)key; 48 | 49 | /* 50 | Getting a SecureUDID can be very expensive. Use this call to derive an identifier in the background, 51 | and invoke a block when ready. Use of this method implies a device running >= iOS 4.0. 52 | 53 | Example usage: 54 | #import "SecureUDID.h" 55 | 56 | [SecureUDID retrieveUDIDForDomain:@"com.example.myapp" usingKey:@"difficult-to-guess-key" completion:^(NSString *identifier) { 57 | // make use of identifier here 58 | }]; 59 | 60 | */ 61 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 62 | + (void)retrieveUDIDForDomain:(NSString *)domain usingKey:(NSString *)key completion:(void (^)(NSString* identifier))completion; 63 | #endif 64 | 65 | /* 66 | Indicates that the system has been disabled via the Opt-Out mechansim. 67 | */ 68 | + (BOOL)isOptedOut; 69 | 70 | @end 71 | 72 | /* 73 | This identifier is returned when Opt-Out is enabled. 74 | */ 75 | extern NSString *const SUUIDDefaultIdentifier; 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](http://www.crashlytics.com/blog/wp-content/uploads/2012/03/SecureUDID.png) 2 | 3 | ####What is SecureUDID? 4 | SecureUDID is an open-source sandboxed device identifier solution aimed at solving the main privacy issues that caused Apple to deprecate UDIDs. 5 | 6 | SecureUDIDs have the following attributes: 7 | 8 | 1. Developers can still differentiate between devices as if they were still using a UDID, but only within apps they control. 9 | 10 | 2. User privacy is protected since developers are fundamentally prevented from accessing the same UDID as another developer. This greatly limits the scope of any potential leaks. 11 | 12 | 3. End-users can globally opt-out of SecureUDID collection across all applications and services that use it. 13 | 14 | ####How do I use it? 15 | 16 | #import "SecureUDID.h" 17 | 18 | NSString *domain = @"com.example.myapp"; 19 | NSString *key = @"difficult-to-guess-key"; 20 | NSString *identifier = [SecureUDID UDIDForDomain:domain usingKey:key]; 21 | // The returned identifier is a 36 character (128 byte + 4 dashes) string that is unique for that domain, key, and device tuple. 22 | 23 | 24 | ####FAQ 25 | 26 | #####Who is behind SecureUDID? 27 | The team at Crashlytics needed to address the UDID situation while still adhering to privacy concerns. Crashlytics wanted to contribute this back to the community. 28 | 29 | #####Is this a true UDID replacement? 30 | SecureUDID has two properties that you should know about before you use it. First, as indicated above, the identifier is not derived from hardware attributes. Second, the persistence of an identifier cannot be guaranteed in all situations. This means that, while unlikely, it is technically possible for two distinct devices to report the same identifier, and for the same device to report different identifiers. Consider this carefully in your application. Here is a list of situations where this identifier will not exhibit the uniqueness/persistence of a traditional UDID. 31 | 32 | - The user has opted-out of the SecureUDID system, in which case you will receive a well-formed string of zeroes. 33 | - Device A is backed up and then restored to Device B, which is an identical model. This is common when someone breaks their phone, for example, and is likely desirable: you will receive Device A's SecureUDID. 34 | - The SecureUDID data is removed, via user intervention, UIPasteboard data purge, or by a malicious application. 35 | - The SecureUDID backing store becomes corrupt. 36 | - All SecureUDID applications are uninstalled from a device, followed by a UIPasteboard data purge. 37 | 38 | #####What about OpenUDID? 39 | AppsFire unveiled OpenUDID back in September as one of the initial responses to Apple's deprecation of UDIDs and our very own Sam Robbins was its second contributor. Since then, we've spent time outlining what would make a more secure UDID, and the changes required turned out to be significant. Establishing a single identifier per device is fundamentally no different than a MAC address or Apple's UDID - the privacy concerns are the same. 40 | 41 | #####Can I use SecureUDID with other UDID frameworks, including OpenUDID? 42 | Yes, SecureUDID does not conflict with any other UDID implementation or framework. 43 | 44 | #####What about Android? 45 | We chose to initially implement SecureUDID on iOS, but the concepts can be applied equally to Android, Windows Phone, and other platforms. We welcome contributions! 46 | 47 | #####How can I get involved? 48 | Fork the crashlytics/secureudid project on GitHub, file issues, implement fixes, and submit pull requests! 49 | 50 | #####Version History 51 | 52 | March 30, 2012 - 1.1 53 | 54 | - Greatly improved robustness to backing store corruption/correctness 55 | - Groundwork for Opt-Out application interface 56 | - Additional API for Opt-Out query and background identifier derivation 57 | - Support for unlimited number of installed SecureUDID applications 58 | - Renamed API to de-emphasize the notion of a salt 59 | - Improved detection of backup/restore 60 | 61 | March 27, 2012 - 1.0 62 | 63 | - Per-Owner dictionary implementation to support Opt-Out functionality 64 | - Addressed an issue that could result in pasteboard overwriting 65 | 66 | March 26, 2012 - 0.9 67 | 68 | - First public release 69 | -------------------------------------------------------------------------------- /SecureUDID.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 221B739C1520A6E1005015FE /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 221B73991520A6CD005015FE /* UIKit.framework */; }; 11 | 221DD6AB152161240023C82A /* SecureUDID.m in Sources */ = {isa = PBXBuildFile; fileRef = 22EB0AF61520A5D5008DB8C8 /* SecureUDID.m */; }; 12 | 22EB0AFF1520A5D5008DB8C8 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22EB0AFE1520A5D5008DB8C8 /* SenTestingKit.framework */; }; 13 | 22EB0B021520A5D5008DB8C8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22EB0AF01520A5D5008DB8C8 /* Foundation.framework */; }; 14 | 22EB0B0B1520A5D5008DB8C8 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 22EB0B091520A5D5008DB8C8 /* InfoPlist.strings */; }; 15 | 22EB0B0E1520A5D5008DB8C8 /* SecureUDIDTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22EB0B0D1520A5D5008DB8C8 /* SecureUDIDTests.m */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 221B73991520A6CD005015FE /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 20 | 22EB0AF01520A5D5008DB8C8 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 21 | 22EB0AF41520A5D5008DB8C8 /* SecureUDID-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SecureUDID-Prefix.pch"; sourceTree = SOURCE_ROOT; }; 22 | 22EB0AF51520A5D5008DB8C8 /* SecureUDID.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SecureUDID.h; sourceTree = SOURCE_ROOT; }; 23 | 22EB0AF61520A5D5008DB8C8 /* SecureUDID.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SecureUDID.m; sourceTree = SOURCE_ROOT; }; 24 | 22EB0AFD1520A5D5008DB8C8 /* SecureUDIDTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecureUDIDTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 22EB0AFE1520A5D5008DB8C8 /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; 26 | 22EB0B081520A5D5008DB8C8 /* SecureUDIDTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SecureUDIDTests-Info.plist"; sourceTree = ""; }; 27 | 22EB0B0A1520A5D5008DB8C8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 28 | 22EB0B0D1520A5D5008DB8C8 /* SecureUDIDTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SecureUDIDTests.m; path = SecureUDIDTests/SecureUDIDTests.m; sourceTree = SOURCE_ROOT; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 22EB0AF91520A5D5008DB8C8 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | 221B739C1520A6E1005015FE /* UIKit.framework in Frameworks */, 37 | 22EB0AFF1520A5D5008DB8C8 /* SenTestingKit.framework in Frameworks */, 38 | 22EB0B021520A5D5008DB8C8 /* Foundation.framework in Frameworks */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | 22EB0AE21520A5D5008DB8C8 = { 46 | isa = PBXGroup; 47 | children = ( 48 | 22EB0AF21520A5D5008DB8C8 /* SecureUDID */, 49 | 22EB0B061520A5D5008DB8C8 /* SecureUDIDTests */, 50 | 22EB0AEF1520A5D5008DB8C8 /* Frameworks */, 51 | 22EB0AEE1520A5D5008DB8C8 /* Products */, 52 | ); 53 | sourceTree = ""; 54 | }; 55 | 22EB0AEE1520A5D5008DB8C8 /* Products */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 22EB0AFD1520A5D5008DB8C8 /* SecureUDIDTests.octest */, 59 | ); 60 | name = Products; 61 | sourceTree = ""; 62 | }; 63 | 22EB0AEF1520A5D5008DB8C8 /* Frameworks */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 221B73991520A6CD005015FE /* UIKit.framework */, 67 | 22EB0AF01520A5D5008DB8C8 /* Foundation.framework */, 68 | 22EB0AFE1520A5D5008DB8C8 /* SenTestingKit.framework */, 69 | ); 70 | name = Frameworks; 71 | sourceTree = ""; 72 | }; 73 | 22EB0AF21520A5D5008DB8C8 /* SecureUDID */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 22EB0AF51520A5D5008DB8C8 /* SecureUDID.h */, 77 | 22EB0AF61520A5D5008DB8C8 /* SecureUDID.m */, 78 | 22EB0AF31520A5D5008DB8C8 /* Supporting Files */, 79 | ); 80 | path = SecureUDID; 81 | sourceTree = ""; 82 | }; 83 | 22EB0AF31520A5D5008DB8C8 /* Supporting Files */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 22EB0AF41520A5D5008DB8C8 /* SecureUDID-Prefix.pch */, 87 | ); 88 | name = "Supporting Files"; 89 | sourceTree = ""; 90 | }; 91 | 22EB0B061520A5D5008DB8C8 /* SecureUDIDTests */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 22EB0B0D1520A5D5008DB8C8 /* SecureUDIDTests.m */, 95 | 22EB0B071520A5D5008DB8C8 /* Supporting Files */, 96 | ); 97 | path = SecureUDIDTests; 98 | sourceTree = ""; 99 | }; 100 | 22EB0B071520A5D5008DB8C8 /* Supporting Files */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 22EB0B081520A5D5008DB8C8 /* SecureUDIDTests-Info.plist */, 104 | 22EB0B091520A5D5008DB8C8 /* InfoPlist.strings */, 105 | ); 106 | name = "Supporting Files"; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | 22EB0AFC1520A5D5008DB8C8 /* SecureUDIDTests */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = 22EB0B141520A5D5008DB8C8 /* Build configuration list for PBXNativeTarget "SecureUDIDTests" */; 115 | buildPhases = ( 116 | 22EB0AF81520A5D5008DB8C8 /* Sources */, 117 | 22EB0AF91520A5D5008DB8C8 /* Frameworks */, 118 | 22EB0AFA1520A5D5008DB8C8 /* Resources */, 119 | 22EB0AFB1520A5D5008DB8C8 /* ShellScript */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = SecureUDIDTests; 126 | productName = SecureUDIDTests; 127 | productReference = 22EB0AFD1520A5D5008DB8C8 /* SecureUDIDTests.octest */; 128 | productType = "com.apple.product-type.bundle"; 129 | }; 130 | /* End PBXNativeTarget section */ 131 | 132 | /* Begin PBXProject section */ 133 | 22EB0AE41520A5D5008DB8C8 /* Project object */ = { 134 | isa = PBXProject; 135 | attributes = { 136 | LastUpgradeCheck = 0430; 137 | ORGANIZATIONNAME = Crashlytics; 138 | }; 139 | buildConfigurationList = 22EB0AE71520A5D5008DB8C8 /* Build configuration list for PBXProject "SecureUDID" */; 140 | compatibilityVersion = "Xcode 3.2"; 141 | developmentRegion = English; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | ); 146 | mainGroup = 22EB0AE21520A5D5008DB8C8; 147 | productRefGroup = 22EB0AEE1520A5D5008DB8C8 /* Products */; 148 | projectDirPath = ""; 149 | projectRoot = ""; 150 | targets = ( 151 | 22EB0AFC1520A5D5008DB8C8 /* SecureUDIDTests */, 152 | ); 153 | }; 154 | /* End PBXProject section */ 155 | 156 | /* Begin PBXResourcesBuildPhase section */ 157 | 22EB0AFA1520A5D5008DB8C8 /* Resources */ = { 158 | isa = PBXResourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 22EB0B0B1520A5D5008DB8C8 /* InfoPlist.strings in Resources */, 162 | ); 163 | runOnlyForDeploymentPostprocessing = 0; 164 | }; 165 | /* End PBXResourcesBuildPhase section */ 166 | 167 | /* Begin PBXShellScriptBuildPhase section */ 168 | 22EB0AFB1520A5D5008DB8C8 /* ShellScript */ = { 169 | isa = PBXShellScriptBuildPhase; 170 | buildActionMask = 2147483647; 171 | files = ( 172 | ); 173 | inputPaths = ( 174 | ); 175 | outputPaths = ( 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | shellPath = /bin/sh; 179 | shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; 180 | }; 181 | /* End PBXShellScriptBuildPhase section */ 182 | 183 | /* Begin PBXSourcesBuildPhase section */ 184 | 22EB0AF81520A5D5008DB8C8 /* Sources */ = { 185 | isa = PBXSourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | 22EB0B0E1520A5D5008DB8C8 /* SecureUDIDTests.m in Sources */, 189 | 221DD6AB152161240023C82A /* SecureUDID.m in Sources */, 190 | ); 191 | runOnlyForDeploymentPostprocessing = 0; 192 | }; 193 | /* End PBXSourcesBuildPhase section */ 194 | 195 | /* Begin PBXVariantGroup section */ 196 | 22EB0B091520A5D5008DB8C8 /* InfoPlist.strings */ = { 197 | isa = PBXVariantGroup; 198 | children = ( 199 | 22EB0B0A1520A5D5008DB8C8 /* en */, 200 | ); 201 | name = InfoPlist.strings; 202 | sourceTree = ""; 203 | }; 204 | /* End PBXVariantGroup section */ 205 | 206 | /* Begin XCBuildConfiguration section */ 207 | 22EB0B0F1520A5D5008DB8C8 /* Debug */ = { 208 | isa = XCBuildConfiguration; 209 | buildSettings = { 210 | ALWAYS_SEARCH_USER_PATHS = NO; 211 | ARCHS = "$(ARCHS_STANDARD_32_BIT)"; 212 | COPY_PHASE_STRIP = NO; 213 | GCC_C_LANGUAGE_STANDARD = gnu99; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_OPTIMIZATION_LEVEL = 0; 216 | GCC_PREPROCESSOR_DEFINITIONS = ( 217 | "DEBUG=1", 218 | "$(inherited)", 219 | ); 220 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 221 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 223 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 224 | GCC_WARN_UNUSED_VARIABLE = YES; 225 | IPHONEOS_DEPLOYMENT_TARGET = 5.1; 226 | SDKROOT = iphoneos; 227 | }; 228 | name = Debug; 229 | }; 230 | 22EB0B101520A5D5008DB8C8 /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | ARCHS = "$(ARCHS_STANDARD_32_BIT)"; 235 | COPY_PHASE_STRIP = YES; 236 | GCC_C_LANGUAGE_STANDARD = gnu99; 237 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 238 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 239 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 240 | GCC_WARN_UNUSED_VARIABLE = YES; 241 | IPHONEOS_DEPLOYMENT_TARGET = 5.1; 242 | SDKROOT = iphoneos; 243 | VALIDATE_PRODUCT = YES; 244 | }; 245 | name = Release; 246 | }; 247 | 22EB0B151520A5D5008DB8C8 /* Debug */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | FRAMEWORK_SEARCH_PATHS = ( 251 | "$(SDKROOT)/Developer/Library/Frameworks", 252 | "$(DEVELOPER_LIBRARY_DIR)/Frameworks", 253 | ); 254 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 255 | GCC_PREFIX_HEADER = "SecureUDID-Prefix.pch"; 256 | INFOPLIST_FILE = "SecureUDIDTests/SecureUDIDTests-Info.plist"; 257 | IPHONEOS_DEPLOYMENT_TARGET = 4.0; 258 | PRODUCT_NAME = "$(TARGET_NAME)"; 259 | RUN_CLANG_STATIC_ANALYZER = YES; 260 | WRAPPER_EXTENSION = octest; 261 | }; 262 | name = Debug; 263 | }; 264 | 22EB0B161520A5D5008DB8C8 /* Release */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | FRAMEWORK_SEARCH_PATHS = ( 268 | "$(SDKROOT)/Developer/Library/Frameworks", 269 | "$(DEVELOPER_LIBRARY_DIR)/Frameworks", 270 | ); 271 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 272 | GCC_PREFIX_HEADER = "SecureUDID-Prefix.pch"; 273 | INFOPLIST_FILE = "SecureUDIDTests/SecureUDIDTests-Info.plist"; 274 | IPHONEOS_DEPLOYMENT_TARGET = 4.0; 275 | PRODUCT_NAME = "$(TARGET_NAME)"; 276 | RUN_CLANG_STATIC_ANALYZER = YES; 277 | WRAPPER_EXTENSION = octest; 278 | }; 279 | name = Release; 280 | }; 281 | /* End XCBuildConfiguration section */ 282 | 283 | /* Begin XCConfigurationList section */ 284 | 22EB0AE71520A5D5008DB8C8 /* Build configuration list for PBXProject "SecureUDID" */ = { 285 | isa = XCConfigurationList; 286 | buildConfigurations = ( 287 | 22EB0B0F1520A5D5008DB8C8 /* Debug */, 288 | 22EB0B101520A5D5008DB8C8 /* Release */, 289 | ); 290 | defaultConfigurationIsVisible = 0; 291 | defaultConfigurationName = Release; 292 | }; 293 | 22EB0B141520A5D5008DB8C8 /* Build configuration list for PBXNativeTarget "SecureUDIDTests" */ = { 294 | isa = XCConfigurationList; 295 | buildConfigurations = ( 296 | 22EB0B151520A5D5008DB8C8 /* Debug */, 297 | 22EB0B161520A5D5008DB8C8 /* Release */, 298 | ); 299 | defaultConfigurationIsVisible = 0; 300 | defaultConfigurationName = Release; 301 | }; 302 | /* End XCConfigurationList section */ 303 | }; 304 | rootObject = 22EB0AE41520A5D5008DB8C8 /* Project object */; 305 | } 306 | -------------------------------------------------------------------------------- /SecureUDIDTests/SecureUDIDTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // SecureUDIDTests.m 3 | // SecureUDID 4 | // 5 | // Created by Crashlytics Team on 3/22/12. 6 | // Copyright (c) 2012 Crashlytics, Inc. All rights reserved. 7 | // http://www.crashlytics.com 8 | // info@crashlytics.com 9 | // 10 | 11 | /* 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | this software and associated documentation files (the "Software"), to deal in 14 | the Software without restriction, including without limitation the rights to 15 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 16 | of the Software, and to permit persons to whom the Software is furnished to do 17 | so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | 31 | #import 32 | #import "SecureUDID.h" 33 | 34 | #import 35 | 36 | // Stuff we need from SecureUDID.m 37 | #define SUUID_SCHEMA_VERSION (1) 38 | #define SUUID_MAX_STORAGE_LOCATIONS (64) 39 | 40 | extern NSString *const SUUIDTypeDataDictionary; 41 | extern NSString *const SUUIDOwnerKey; 42 | extern NSString *const SUUIDIdentifierKey; 43 | extern NSString *const SUUIDOptOutKey; 44 | extern NSString *const SUUIDTimeStampKey; 45 | extern NSString *const SUUIDModelHashKey; 46 | extern NSString *const SUUIDSchemaVersionKey; 47 | 48 | extern void SUUIDMarkOptedOut(void); 49 | extern void SUUIDMarkOptedIn(void); 50 | extern NSString *SUUIDPasteboardNameForNumber(NSInteger number); 51 | extern NSDictionary *SUUIDDictionaryForStorageLocation(NSInteger number); 52 | extern void SUUIDWriteDictionaryToStorageLocation(NSInteger number, NSDictionary* dictionary); 53 | extern void SUUIDDeleteStorageLocation(NSInteger number); 54 | extern void SUUIDRemoveAllSecureUDIDData(void); 55 | 56 | // End of stuff 57 | 58 | @interface SecureUDIDTests : SenTestCase 59 | 60 | - (void)setUp; 61 | 62 | @end 63 | 64 | @implementation SecureUDIDTests 65 | 66 | - (void)setUp { 67 | 68 | // clear out any previous pasteboards 69 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 70 | SUUIDDeleteStorageLocation(i); 71 | } 72 | } 73 | 74 | - (void)writeUnverifiedData:(NSDictionary*)dictionary toStorageLocation:(NSInteger)location { 75 | UIPasteboard* pasteboard; 76 | 77 | pasteboard = [UIPasteboard pasteboardWithName:SUUIDPasteboardNameForNumber(location) create:YES]; 78 | if (!pasteboard) { 79 | return; 80 | } 81 | 82 | pasteboard.persistent = YES; 83 | 84 | [pasteboard setData:[NSKeyedArchiver archivedDataWithRootObject:dictionary] 85 | forPasteboardType:SUUIDTypeDataDictionary]; 86 | } 87 | 88 | /* 89 | Tests the output from the UDIDForDomain:usingKey: method. 90 | */ 91 | - (void)testUDIDForDomain { 92 | // Confirm we get a UDID back. 93 | NSString *udid = [SecureUDID UDIDForDomain:@"com.example.myapp" usingKey:@"superSecretCodeHere!@##%#$#%$^"]; 94 | STAssertNotNil(udid, @"udid should not be nil"); 95 | 96 | // Confirm we get the same UDID back. 97 | NSString *sameUDID = [SecureUDID UDIDForDomain:@"com.example.myapp" usingKey:@"superSecretCodeHere!@##%#$#%$^"]; 98 | STAssertNotNil(sameUDID, @"sameUDID should not be nil"); 99 | STAssertEqualObjects(udid, sameUDID, @"udid and sameUDID should be equal"); 100 | 101 | // Confirm we get a different UDID since we are using a different domain. 102 | NSString *newUDID = [SecureUDID UDIDForDomain:@"com.example.myapp.udid" usingKey:@"superSecretCodeHere!@##%#$#%$^"]; 103 | STAssertNotNil(newUDID, @"newUDID should not be nil"); 104 | STAssertFalse([newUDID isEqualToString:udid], @"newUDID and udid should not be equal"); 105 | } 106 | 107 | - (void)testFirstDomainGetsFirstLocation { 108 | NSDictionary* dictionary; 109 | 110 | [SecureUDID UDIDForDomain:@"com.example.myapp" usingKey:@"example key"]; 111 | 112 | dictionary = SUUIDDictionaryForStorageLocation(0); 113 | STAssertNotNil(dictionary, @"The first location should have a valid dictionary"); 114 | STAssertNotNil([dictionary objectForKey:[dictionary objectForKey:SUUIDOwnerKey]], @"The owner should have an owner dictionary"); 115 | 116 | for (NSInteger i = 1; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 117 | dictionary = SUUIDDictionaryForStorageLocation(i); 118 | 119 | STAssertNil(dictionary, @"All other locations should be nil"); 120 | } 121 | } 122 | 123 | - (void)testSecondDomainDoesNotOverwriteFirst { 124 | NSDictionary* dictionary1; 125 | NSDictionary* dictionary2; 126 | 127 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 128 | [SecureUDID UDIDForDomain:@"com.example.myapp-2" usingKey:@"example key 2"]; 129 | 130 | dictionary1 = SUUIDDictionaryForStorageLocation(0); 131 | dictionary2 = SUUIDDictionaryForStorageLocation(1); 132 | 133 | STAssertEquals(5, (int)[dictionary1 count], @"First dictionary should have four meta-data keys + one owner"); 134 | STAssertNotNil([dictionary1 objectForKey:SUUIDOwnerKey], @"An owner should be set"); 135 | STAssertNotNil([dictionary1 objectForKey:SUUIDTimeStampKey], @"A timestamp should be set too"); 136 | 137 | STAssertEquals(6, (int)[dictionary2 count], @"Second dictionary should have four meta-data keys + two owners"); 138 | STAssertNotNil([dictionary2 objectForKey:SUUIDOwnerKey], @"An owner should be set"); 139 | STAssertNotNil([dictionary2 objectForKey:SUUIDTimeStampKey], @"A timestamp should be set too"); 140 | 141 | STAssertFalse([[dictionary1 objectForKey:SUUIDOwnerKey] isEqual:[dictionary2 objectForKey:SUUIDOwnerKey]], @"Owners should not be equal"); 142 | 143 | STAssertNotNil([dictionary1 objectForKey:[dictionary1 objectForKey:SUUIDOwnerKey]], @"The owner of the first should be represented in the first"); 144 | STAssertNil([dictionary1 objectForKey:[dictionary2 objectForKey:SUUIDOwnerKey]], @"The owner of the second should NOT be represented in the first"); 145 | 146 | STAssertNotNil([dictionary2 objectForKey:[dictionary1 objectForKey:SUUIDOwnerKey]], @"The owner of the first should be represented in the second"); 147 | STAssertNotNil([dictionary2 objectForKey:[dictionary2 objectForKey:SUUIDOwnerKey]], @"The owner of the second should be represented in the second"); 148 | } 149 | 150 | - (void)testSecondDomainCopiedToFirstOnAccess { 151 | NSDictionary* dictionary1; 152 | NSDictionary* dictionary2; 153 | NSData* owner1; 154 | NSData* owner2; 155 | 156 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 157 | [SecureUDID UDIDForDomain:@"com.example.myapp-2" usingKey:@"example key 2"]; 158 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 159 | 160 | dictionary1 = SUUIDDictionaryForStorageLocation(0); 161 | dictionary2 = SUUIDDictionaryForStorageLocation(1); 162 | 163 | owner1 = [dictionary1 objectForKey:SUUIDOwnerKey]; 164 | owner2 = [dictionary2 objectForKey:SUUIDOwnerKey]; 165 | 166 | STAssertNotNil([dictionary1 objectForKey:owner1], @"Owner1 should be represented in the first"); 167 | STAssertNotNil([dictionary1 objectForKey:owner2], @"Owner2 should be represented in the first"); 168 | } 169 | 170 | - (void)testMaximumPlusOneAccess { 171 | NSDictionary* firstDictionary; 172 | NSDictionary* dictionary; 173 | 174 | // This takes a long time to execute. The storage algorithm is inefficient, so calling it 175 | // many times in a row isn't great. The typical case, where an application generates one (or just a few) 176 | // identifiers is much better. 177 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 178 | [SecureUDID UDIDForDomain:[NSString stringWithFormat:@"com.example.myapp-%d", i] usingKey:@"example key"]; 179 | } 180 | 181 | // that should be the max 182 | firstDictionary = SUUIDDictionaryForStorageLocation(0); 183 | dictionary = SUUIDDictionaryForStorageLocation(SUUID_MAX_STORAGE_LOCATIONS-1); 184 | STAssertNotNil(dictionary, @"The last storage location should have an entry"); 185 | 186 | // add the plus one 187 | [SecureUDID UDIDForDomain:@"com.example.myapp-last-plus-one" usingKey:@"example key"]; 188 | 189 | // this should overwrite the firstDictionary location 190 | dictionary = SUUIDDictionaryForStorageLocation(0); 191 | STAssertFalse([[dictionary objectForKey:SUUIDOwnerKey] isEqual:[firstDictionary objectForKey:SUUIDOwnerKey]], @"Owners should not be equal"); 192 | STAssertEquals(SUUID_MAX_STORAGE_LOCATIONS + 1 + 4, (int)[dictionary count], @"All owners, plus four meta-data entries, should be present"); 193 | } 194 | 195 | - (void)testCorruptionViaMissingTimestamp { 196 | NSMutableDictionary* dictionary; 197 | 198 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 199 | 200 | dictionary = [NSMutableDictionary dictionaryWithDictionary:SUUIDDictionaryForStorageLocation(0)]; 201 | 202 | [dictionary removeObjectForKey:SUUIDTimeStampKey]; 203 | 204 | [self writeUnverifiedData:dictionary toStorageLocation:0]; 205 | 206 | STAssertNil(SUUIDDictionaryForStorageLocation(0), @"Location should be removed"); 207 | } 208 | 209 | - (void)testCorruptionViaMissingOwner { 210 | NSMutableDictionary* dictionary; 211 | 212 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 213 | 214 | dictionary = [NSMutableDictionary dictionaryWithDictionary:SUUIDDictionaryForStorageLocation(0)]; 215 | 216 | [dictionary removeObjectForKey:SUUIDOwnerKey]; 217 | 218 | [self writeUnverifiedData:dictionary toStorageLocation:0]; 219 | 220 | STAssertNil(SUUIDDictionaryForStorageLocation(0), @"Location should be removed"); 221 | } 222 | 223 | - (void)testCorruptionViaDecryptionFailureOfIndentifier { 224 | NSString* identifier; 225 | NSMutableDictionary* dictionary; 226 | NSMutableDictionary* ownerDictionary; 227 | 228 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 229 | STAssertFalse([identifier isEqualToString:SUUIDDefaultIdentifier], @"Id should not be the default"); 230 | 231 | dictionary = [NSMutableDictionary dictionaryWithDictionary:SUUIDDictionaryForStorageLocation(0)]; 232 | ownerDictionary = [NSMutableDictionary dictionaryWithDictionary:[dictionary objectForKey:[dictionary objectForKey:SUUIDOwnerKey]]]; 233 | 234 | [ownerDictionary setObject:[NSData data] forKey:SUUIDIdentifierKey]; // set a bogus identifier 235 | [dictionary setObject:ownerDictionary forKey:[dictionary objectForKey:SUUIDOwnerKey]]; 236 | 237 | [self writeUnverifiedData:dictionary toStorageLocation:0]; 238 | 239 | // after all that, get the id back so we can check it 240 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 241 | STAssertEqualObjects(SUUIDDefaultIdentifier, identifier, @"The default id should come back on decryption failure"); 242 | STAssertNil(SUUIDDictionaryForStorageLocation(0), @"... and the storage location should get blown away"); 243 | } 244 | 245 | - (void)testNewUDIDGeneratedWithModelHashMismatch { 246 | NSMutableDictionary* dictionary; 247 | NSString* identifier1; 248 | NSString* identifier2; 249 | 250 | identifier1 = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 251 | 252 | dictionary = [NSMutableDictionary dictionaryWithDictionary:SUUIDDictionaryForStorageLocation(0)]; 253 | 254 | [dictionary setObject:[NSData data] forKey:SUUIDModelHashKey]; 255 | 256 | SUUIDWriteDictionaryToStorageLocation(0, dictionary); 257 | STAssertNotNil(SUUIDDictionaryForStorageLocation(0), @"Mismatched model hash should still pass verification"); 258 | 259 | identifier2 = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 260 | 261 | STAssertNil(SUUIDDictionaryForStorageLocation(1), @"Should still write to first location"); 262 | STAssertFalse([identifier1 isEqual:identifier2], @"IDs should not be the same"); 263 | } 264 | 265 | - (void)testOptOutProvidesDefaultIdentifier { 266 | NSString* identifier; 267 | 268 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 269 | 270 | STAssertFalse([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should not be the default"); 271 | 272 | SUUIDMarkOptedOut(); 273 | 274 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 275 | 276 | STAssertTrue([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should now be the default"); 277 | } 278 | 279 | - (void)testOptOutPreservesGeneratedIDs { 280 | NSDictionary* dictionary; 281 | NSString* identifier; 282 | 283 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 284 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-2" usingKey:@"example key 2"]; 285 | 286 | SUUIDMarkOptedOut(); 287 | 288 | STAssertTrue([SecureUDID isOptedOut], @"Should now be opted out"); 289 | 290 | // All storage locations should have been updated here 291 | dictionary = SUUIDDictionaryForStorageLocation(0); 292 | 293 | STAssertEquals(7, (int)[dictionary count], @"There should be five meta-data keys and two owners"); 294 | 295 | SUUIDMarkOptedIn(); 296 | 297 | STAssertEqualObjects(identifier, [SecureUDID UDIDForDomain:@"com.example.myapp-2" usingKey:@"example key 2"], @"Identifiers should remain the same"); 298 | } 299 | 300 | - (void)testOptOutPreservesOwners { 301 | NSData* owner1Before; 302 | NSData* owner1After; 303 | NSData* owner2Before; 304 | NSData* owner2After; 305 | 306 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 307 | [SecureUDID UDIDForDomain:@"com.example.myapp-2" usingKey:@"example key 2"]; 308 | 309 | owner1Before = [SUUIDDictionaryForStorageLocation(0) objectForKey:SUUIDOwnerKey]; 310 | owner2Before = [SUUIDDictionaryForStorageLocation(1) objectForKey:SUUIDOwnerKey]; 311 | 312 | STAssertFalse([owner1Before isEqualToData:owner2Before], @"Owners should be distinct"); 313 | 314 | SUUIDMarkOptedOut(); 315 | 316 | owner1After = [SUUIDDictionaryForStorageLocation(0) objectForKey:SUUIDOwnerKey]; 317 | owner2After = [SUUIDDictionaryForStorageLocation(1) objectForKey:SUUIDOwnerKey]; 318 | 319 | STAssertEqualObjects(owner1Before, owner1After, @"Onwer 1 should be the same"); 320 | STAssertEqualObjects(owner2Before, owner2After, @"Onwer 1 should be the same"); 321 | } 322 | 323 | - (void)testOptInWithNoData { 324 | NSString* identifier; 325 | 326 | SUUIDMarkOptedIn(); 327 | 328 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 329 | STAssertNil(SUUIDDictionaryForStorageLocation(i), @"No data should be present if you opt-in without any ids established"); 330 | } 331 | 332 | STAssertFalse([SecureUDID isOptedOut], @"Should not be opted out"); 333 | 334 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 335 | 336 | STAssertFalse([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should not be the default"); 337 | } 338 | 339 | - (void)testOptOutWithNoData { 340 | NSString* identifier; 341 | 342 | SUUIDMarkOptedOut(); 343 | 344 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 345 | STAssertNotNil(SUUIDDictionaryForStorageLocation(i), @"No data should be present if you opt-in without any ids established"); 346 | } 347 | 348 | STAssertTrue([SecureUDID isOptedOut], @"Should be opted out"); 349 | 350 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 351 | 352 | STAssertTrue([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should be the default"); 353 | } 354 | 355 | - (void)testOptOutTwiceWithNoData { 356 | NSString* identifier; 357 | 358 | SUUIDMarkOptedOut(); 359 | SUUIDMarkOptedOut(); 360 | 361 | STAssertTrue([SecureUDID isOptedOut], @"Should be opted out"); 362 | 363 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 364 | 365 | STAssertTrue([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should be the default"); 366 | } 367 | 368 | - (void)testOptOutTwice { 369 | NSString* identifier; 370 | 371 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 372 | 373 | SUUIDMarkOptedOut(); 374 | SUUIDMarkOptedOut(); 375 | 376 | STAssertTrue([SecureUDID isOptedOut], @"Should be opted out"); 377 | 378 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 379 | 380 | STAssertTrue([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should be the default"); 381 | } 382 | 383 | - (void)testOptOutFollowedByDeleteShouldHaveOptOutInAllLocations { 384 | NSString* identifier; 385 | 386 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 387 | 388 | SUUIDMarkOptedOut(); 389 | 390 | SUUIDRemoveAllSecureUDIDData(); 391 | 392 | STAssertTrue([SecureUDID isOptedOut], @"Should be opted out"); 393 | 394 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 395 | NSDictionary* dictionary; 396 | 397 | dictionary = SUUIDDictionaryForStorageLocation(i); 398 | STAssertTrue([[dictionary objectForKey:SUUIDOptOutKey] boolValue], @"Opt-Out flag should be set"); 399 | } 400 | 401 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 402 | 403 | STAssertTrue([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should be the default"); 404 | } 405 | 406 | - (void)testDeleteFollowedByOptOutShouldHaveOptOutInAllLocations { 407 | NSString* identifier; 408 | 409 | [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 410 | 411 | SUUIDRemoveAllSecureUDIDData(); 412 | 413 | SUUIDMarkOptedOut(); 414 | 415 | STAssertTrue([SecureUDID isOptedOut], @"Should be opted out"); 416 | 417 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 418 | NSDictionary* dictionary; 419 | 420 | dictionary = SUUIDDictionaryForStorageLocation(i); 421 | STAssertTrue([[dictionary objectForKey:SUUIDOptOutKey] boolValue], @"Opt-Out flag should be set"); 422 | } 423 | 424 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 425 | 426 | STAssertTrue([identifier isEqualToString:SUUIDDefaultIdentifier], @"Identifier should be the default"); 427 | } 428 | 429 | - (void)testNewerSchemaIsUntouched { 430 | NSString* identifier; 431 | NSMutableDictionary* dictionary; 432 | 433 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 434 | STAssertFalse([identifier isEqualToString:SUUIDDefaultIdentifier], @"Should not be the default id"); 435 | 436 | dictionary = [NSMutableDictionary dictionaryWithDictionary:SUUIDDictionaryForStorageLocation(0)]; 437 | 438 | [dictionary setObject:[NSNumber numberWithInt:SUUID_SCHEMA_VERSION+1] forKey:SUUIDSchemaVersionKey]; 439 | 440 | [self writeUnverifiedData:dictionary toStorageLocation:0]; // save it to the store 441 | 442 | identifier = [SecureUDID UDIDForDomain:@"com.example.myapp-1" usingKey:@"example key 1"]; 443 | STAssertTrue([identifier isEqualToString:SUUIDDefaultIdentifier], @"Should be the default when schemas don't match"); 444 | 445 | STAssertTrue([dictionary isEqual:SUUIDDictionaryForStorageLocation(0)], @"And the dictionary should be identical"); 446 | } 447 | 448 | @end 449 | -------------------------------------------------------------------------------- /SecureUDID.m: -------------------------------------------------------------------------------- 1 | // 2 | // SecureUDID.m 3 | // SecureUDID 4 | // 5 | // Created by Crashlytics Team on 3/22/12. 6 | // Copyright (c) 2012 Crashlytics, Inc. All rights reserved. 7 | // http://www.crashlytics.com 8 | // info@crashlytics.com 9 | // 10 | 11 | /* 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | this software and associated documentation files (the "Software"), to deal in 14 | the Software without restriction, including without limitation the rights to 15 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 16 | of the Software, and to permit persons to whom the Software is furnished to do 17 | so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | 31 | #import "SecureUDID.h" 32 | #import 33 | #import 34 | #import 35 | #import 36 | 37 | #define SUUID_SCHEMA_VERSION (1) 38 | #define SUUID_MAX_STORAGE_LOCATIONS (64) 39 | 40 | NSString *const SUUIDDefaultIdentifier = @"00000000-0000-0000-0000-000000000000"; 41 | 42 | NSString *const SUUIDTypeDataDictionary = @"public.secureudid"; 43 | NSString *const SUUIDTimeStampKey = @"SUUIDTimeStampKey"; 44 | NSString *const SUUIDOwnerKey = @"SUUIDOwnerKey"; 45 | NSString *const SUUIDLastAccessedKey = @"SUUIDLastAccessedKey"; 46 | NSString *const SUUIDIdentifierKey = @"SUUIDIdentifierKey"; 47 | NSString *const SUUIDOptOutKey = @"SUUIDOptOutKey"; 48 | NSString *const SUUIDModelHashKey = @"SUUIDModelHashKey"; 49 | NSString *const SUUIDSchemaVersionKey = @"SUUIDSchemaVersionKey"; 50 | NSString *const SUUIDPastboardFileFormat = @"org.secureudid-%d"; 51 | 52 | NSData *SUUIDCryptorToData(CCOperation operation, NSData *value, NSData *key); 53 | NSString *SUUIDCryptorToString(CCOperation operation, NSData *value, NSData *key); 54 | NSData *SUUIDHash(NSData* data); 55 | NSData *SUUIDModelHash(void); 56 | 57 | void SUUIDMarkOptedOut(void); 58 | void SUUIDMarkOptedIn(void); 59 | void SUUIDRemoveAllSecureUDIDData(void); 60 | NSString *SUUIDPasteboardNameForNumber(NSInteger number); 61 | NSInteger SUUIDStorageLocationForOwnerKey(NSData *key, NSMutableDictionary** dictionary); 62 | NSDictionary *SUUIDDictionaryForStorageLocation(NSInteger number); 63 | NSDictionary *SUUIDMostRecentDictionary(void); 64 | void SUUIDWriteDictionaryToStorageLocation(NSInteger number, NSDictionary* dictionary); 65 | void SUUIDDeleteStorageLocation(NSInteger number); 66 | 67 | BOOL SUUIDValidTopLevelObject(id object); 68 | BOOL SUUIDValidOwnerObject(id object); 69 | 70 | @implementation SecureUDID 71 | 72 | /* 73 | Returns a unique id for the device, sandboxed to the domain and salt provided. 74 | 75 | Example usage: 76 | #import "SecureUDID.h" 77 | 78 | NSString *udid = [SecureUDID UDIDForDomain:@"com.example.myapp" salt:@"superSecretCodeHere!@##%#$#%$^"]; 79 | 80 | */ 81 | + (NSString *)UDIDForDomain:(NSString *)domain usingKey:(NSString *)key { 82 | NSString *identifier = SUUIDDefaultIdentifier; 83 | 84 | // Salt the domain to make the crypt keys affectively unguessable. 85 | NSData *domainAndKey = [[NSString stringWithFormat:@"%@%@", domain, key] dataUsingEncoding:NSUTF8StringEncoding]; 86 | NSData *ownerKey = SUUIDHash(domainAndKey); 87 | 88 | // Encrypt the salted domain key and load the pasteboard on which to store data 89 | NSData *encryptedOwnerKey = SUUIDCryptorToData(kCCEncrypt, [domain dataUsingEncoding:NSUTF8StringEncoding], ownerKey); 90 | 91 | // @synchronized introduces an implicit @try-@finally, so care needs to be taken with the return value 92 | @synchronized (self) { 93 | NSMutableDictionary *topLevelDictionary = nil; 94 | 95 | // Retrieve an appropriate storage index for this owner 96 | NSInteger ownerIndex = SUUIDStorageLocationForOwnerKey(encryptedOwnerKey, &topLevelDictionary); 97 | 98 | // If the model hash key is present, verify it, otherwise add it 99 | NSData *storedModelHash = [topLevelDictionary objectForKey:SUUIDModelHashKey]; 100 | NSData *modelHash = SUUIDModelHash(); 101 | 102 | if (storedModelHash) { 103 | if (![modelHash isEqual:storedModelHash]) { 104 | // The model hashes do not match - this structure is invalid 105 | [topLevelDictionary removeAllObjects]; 106 | } 107 | } 108 | 109 | // store the current model hash 110 | [topLevelDictionary setObject:modelHash forKey:SUUIDModelHashKey]; 111 | 112 | // check for the opt-out flag and return the default identifier if we find it 113 | if ([[topLevelDictionary objectForKey:SUUIDOptOutKey] boolValue] == YES) { 114 | return identifier; 115 | } 116 | 117 | // If we encounter a schema version greater than we support, there is no simple alternative 118 | // other than to simulate Opt Out. Any writes to the store risk corruption. 119 | if ([[topLevelDictionary objectForKey:SUUIDSchemaVersionKey] intValue] > SUUID_SCHEMA_VERSION) { 120 | return identifier; 121 | } 122 | 123 | // Attempt to get the owner's dictionary. Should we get back nil from the encryptedDomain key, we'll still 124 | // get a valid, empty mutable dictionary 125 | NSMutableDictionary *ownerDictionary = [NSMutableDictionary dictionaryWithDictionary:[topLevelDictionary objectForKey:encryptedOwnerKey]]; 126 | 127 | // Set our last access time and claim ownership for this storage location. 128 | NSDate* lastAccessDate = [NSDate date]; 129 | 130 | [ownerDictionary setObject:lastAccessDate forKey:SUUIDLastAccessedKey]; 131 | [topLevelDictionary setObject:lastAccessDate forKey:SUUIDTimeStampKey]; 132 | [topLevelDictionary setObject:encryptedOwnerKey forKey:SUUIDOwnerKey]; 133 | 134 | [topLevelDictionary setObject:[NSNumber numberWithInt:SUUID_SCHEMA_VERSION] forKey:SUUIDSchemaVersionKey]; 135 | 136 | // Make sure our owner dictionary is in the top level structure 137 | [topLevelDictionary setObject:ownerDictionary forKey:encryptedOwnerKey]; 138 | 139 | 140 | NSData *identifierData = [ownerDictionary objectForKey:SUUIDIdentifierKey]; 141 | if (identifierData) { 142 | identifier = SUUIDCryptorToString(kCCDecrypt, identifierData, ownerKey); 143 | if (!identifier) { 144 | // We've failed to decrypt our identifier. This is a sign of storage corruption. 145 | SUUIDDeleteStorageLocation(ownerIndex); 146 | 147 | // return here - do not write values back to the store 148 | return SUUIDDefaultIdentifier; 149 | } 150 | } else { 151 | // Otherwise, create a new RFC-4122 Version 4 UUID 152 | // http://en.wikipedia.org/wiki/Universally_unique_identifier 153 | CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); 154 | identifier = [(NSString*)CFUUIDCreateString(kCFAllocatorDefault, uuid) autorelease]; 155 | CFRelease(uuid); 156 | 157 | // Encrypt it for storage. 158 | NSData *data = SUUIDCryptorToData(kCCEncrypt, [identifier dataUsingEncoding:NSUTF8StringEncoding], ownerKey); 159 | 160 | [ownerDictionary setObject:data forKey:SUUIDIdentifierKey]; 161 | } 162 | 163 | SUUIDWriteDictionaryToStorageLocation(ownerIndex, topLevelDictionary); 164 | } 165 | 166 | return identifier; 167 | } 168 | 169 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 170 | + (void)retrieveUDIDForDomain:(NSString *)domain usingKey:(NSString *)key completion:(void (^)(NSString* identifier))completion { 171 | // retreive the identifier on a low-priority thread 172 | 173 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 174 | NSString* identifier; 175 | 176 | identifier = [SecureUDID UDIDForDomain:domain usingKey:key]; 177 | 178 | completion(identifier); 179 | }); 180 | } 181 | #endif 182 | 183 | /* 184 | API to determine if a device has opted out of SecureUDID. 185 | */ 186 | + (BOOL)isOptedOut { 187 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 188 | NSDictionary* topLevelDictionary; 189 | 190 | topLevelDictionary = SUUIDDictionaryForStorageLocation(i); 191 | if (!topLevelDictionary) { 192 | continue; 193 | } 194 | 195 | if ([[topLevelDictionary objectForKey:SUUIDOptOutKey] boolValue] == YES) { 196 | return YES; 197 | } 198 | } 199 | 200 | return NO; 201 | } 202 | 203 | /* 204 | Applies the operation (encrypt or decrypt) to the NSData value with the provided NSData key 205 | and returns the value as NSData. 206 | */ 207 | NSData *SUUIDCryptorToData(CCOperation operation, NSData *value, NSData *key) { 208 | NSMutableData *output = [NSMutableData dataWithLength:value.length + kCCBlockSizeAES128]; 209 | 210 | size_t numBytes = 0; 211 | CCCryptorStatus cryptStatus = CCCrypt(operation, 212 | kCCAlgorithmAES128, 213 | kCCOptionPKCS7Padding, 214 | [key bytes], 215 | kCCKeySizeAES128, 216 | NULL, 217 | value.bytes, 218 | value.length, 219 | output.mutableBytes, 220 | output.length, 221 | &numBytes); 222 | 223 | if (cryptStatus == kCCSuccess) { 224 | return [[[NSData alloc] initWithBytes:output.bytes length:numBytes] autorelease]; 225 | } 226 | 227 | return nil; 228 | } 229 | 230 | /* 231 | Applies the operation (encrypt or decrypt) to the NSData value with the provided NSData key 232 | and returns the value as an NSString. 233 | */ 234 | NSString *SUUIDCryptorToString(CCOperation operation, NSData *value, NSData *key) { 235 | NSData* data; 236 | 237 | data = SUUIDCryptorToData(operation, value, key); 238 | if (!data) { 239 | return nil; 240 | } 241 | 242 | return [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; 243 | } 244 | 245 | /* 246 | Compute a SHA1 of the input. 247 | */ 248 | NSData *SUUIDHash(NSData __unsafe_unretained * data) { 249 | uint8_t digest[CC_SHA1_DIGEST_LENGTH] = {0}; 250 | 251 | CC_SHA1(data.bytes, data.length, digest); 252 | 253 | return [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH]; 254 | } 255 | 256 | NSData* SUUIDModelHash(void) { 257 | NSString* result; 258 | 259 | result = @"Unknown"; 260 | 261 | do { 262 | size_t size; 263 | char* value; 264 | 265 | value = NULL; 266 | 267 | // first get the size 268 | if (sysctlbyname("hw.machine", NULL, &size, NULL, 0) != 0) { 269 | break; 270 | } 271 | 272 | value = malloc(size); 273 | if (!value) { 274 | break; 275 | } 276 | 277 | // now get the value 278 | if (sysctlbyname("hw.machine", value, &size, NULL, 0) != 0) { 279 | break; 280 | } 281 | 282 | // convert the value to an NSString 283 | result = [NSString stringWithCString:value encoding:NSUTF8StringEncoding]; 284 | if (!result) { 285 | break; 286 | } 287 | 288 | // free our buffer 289 | free(value); 290 | } while (0); 291 | 292 | return SUUIDHash([result dataUsingEncoding:NSUTF8StringEncoding]); 293 | } 294 | 295 | /* 296 | Finds the most recent structure, and adds the Opt-Out flag to it. Then writes that structure back 297 | out to all used storage locations, making sure to preserve ownership. 298 | */ 299 | void SUUIDMarkOptedOut(void) { 300 | NSMutableDictionary* mostRecentDictionary; 301 | 302 | mostRecentDictionary = [NSMutableDictionary dictionaryWithDictionary:SUUIDMostRecentDictionary()]; 303 | 304 | [mostRecentDictionary setObject:[NSDate date] forKey:SUUIDTimeStampKey]; 305 | [mostRecentDictionary setObject:[NSNumber numberWithBool:YES] forKey:SUUIDOptOutKey]; 306 | 307 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 308 | NSData* owner; 309 | 310 | // Inherit the owner, if it is present. This makes some schema assumptions. 311 | owner = [SUUIDDictionaryForStorageLocation(i) objectForKey:SUUIDOwnerKey]; 312 | if (owner) { 313 | [mostRecentDictionary setObject:owner forKey:SUUIDOwnerKey]; 314 | } 315 | 316 | // write the opt-out data even if the location was previously empty 317 | SUUIDWriteDictionaryToStorageLocation(i, mostRecentDictionary); 318 | } 319 | } 320 | 321 | void SUUIDMarkOptedIn(void) { 322 | NSDate* accessedDate; 323 | 324 | accessedDate = [NSDate date]; 325 | 326 | // Opting back in is trickier. We need to remove top-level Opt-Out markers. Also makes some 327 | // schema assumptions. 328 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 329 | NSMutableDictionary* dictionary; 330 | 331 | dictionary = [NSMutableDictionary dictionaryWithDictionary:SUUIDDictionaryForStorageLocation(i)]; 332 | if (!dictionary) { 333 | // This is a possible indiction of storage corruption. However, SUUIDDictionaryForStorageLocation 334 | // will have already cleaned it up for us, so there's not much to do here. 335 | continue; 336 | } 337 | 338 | [dictionary removeObjectForKey:SUUIDOptOutKey]; 339 | 340 | // quick check for the minimum set of keys. If the dictionary previously held just 341 | // an Opt-Out marker + timestamp, dictionary is not invalid. Writing will fail in this 342 | // case, leaving the data that was there. We need to delete. 343 | if (!SUUIDValidTopLevelObject(dictionary)) { 344 | SUUIDDeleteStorageLocation(i); 345 | continue; 346 | } 347 | 348 | [dictionary setObject:accessedDate forKey:SUUIDTimeStampKey]; 349 | 350 | SUUIDWriteDictionaryToStorageLocation(i, dictionary); 351 | } 352 | } 353 | 354 | /* 355 | Removes all SecureUDID data from storage with the exception of Opt-Out flags, which 356 | are never removed. Removing the Opt-Out flags would effectively opt a user back in. 357 | */ 358 | void SUUIDRemoveAllSecureUDIDData(void) { 359 | NSDictionary* optOutPlaceholder = nil; 360 | 361 | if ([SecureUDID isOptedOut]) { 362 | optOutPlaceholder = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:SUUIDOptOutKey]; 363 | } 364 | 365 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 366 | if (optOutPlaceholder) { 367 | SUUIDWriteDictionaryToStorageLocation(i, optOutPlaceholder); 368 | continue; 369 | } 370 | 371 | SUUIDDeleteStorageLocation(i); 372 | } 373 | } 374 | 375 | /* 376 | Returns an NSString formatted with the supplied number. 377 | */ 378 | NSString *SUUIDPasteboardNameForNumber(NSInteger number) { 379 | return [NSString stringWithFormat:SUUIDPastboardFileFormat, number]; 380 | } 381 | 382 | /* 383 | Reads a dictionary from a storage location. Validation occurs once data 384 | is read, but before it is returned. If something fails, or if the read structure 385 | is invalid, the location is cleared. 386 | 387 | Returns the data dictionary, or nil on failure. 388 | */ 389 | NSDictionary *SUUIDDictionaryForStorageLocation(NSInteger number) { 390 | id decodedObject; 391 | UIPasteboard* pasteboard; 392 | NSData* data; 393 | 394 | // Don't even bother if the index is outside our limits 395 | if (number < 0 || number >= SUUID_MAX_STORAGE_LOCATIONS) { 396 | return nil; 397 | } 398 | 399 | pasteboard = [UIPasteboard pasteboardWithName:SUUIDPasteboardNameForNumber(number) create:NO]; 400 | if (!pasteboard) { 401 | return nil; 402 | } 403 | 404 | data = [pasteboard valueForPasteboardType:SUUIDTypeDataDictionary]; 405 | if (!data) { 406 | return nil; 407 | } 408 | 409 | @try { 410 | decodedObject = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 411 | } @catch (NSException* exception) { 412 | // Catching an exception like this is risky. However, crashing here is 413 | // not acceptable, and unarchiveObjectWithData can throw. 414 | [pasteboard setData:nil forPasteboardType:SUUIDTypeDataDictionary]; 415 | 416 | return nil; 417 | } 418 | 419 | if (!SUUIDValidTopLevelObject(decodedObject)) { 420 | [pasteboard setData:nil forPasteboardType:SUUIDTypeDataDictionary]; 421 | 422 | return nil; 423 | } 424 | 425 | return decodedObject; 426 | } 427 | 428 | NSDictionary *SUUIDMostRecentDictionary(void) { 429 | NSDictionary* mostRecentDictionary; 430 | BOOL found; 431 | 432 | mostRecentDictionary = [NSDictionary dictionaryWithObject:[NSDate distantPast] forKey:SUUIDTimeStampKey]; 433 | 434 | // scan all locations looking for the most recent 435 | for (NSUInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 436 | NSDictionary* dictionary; 437 | NSDate* date; 438 | 439 | dictionary = SUUIDDictionaryForStorageLocation(i); 440 | if (!dictionary) { 441 | continue; 442 | } 443 | 444 | // Schema assumption 445 | date = [dictionary objectForKey:SUUIDTimeStampKey]; 446 | if ([date compare:[mostRecentDictionary objectForKey:SUUIDTimeStampKey]] == NSOrderedDescending) { 447 | mostRecentDictionary = dictionary; 448 | found = YES; 449 | } 450 | } 451 | 452 | if (!found) { 453 | return nil; 454 | } 455 | 456 | return mostRecentDictionary; 457 | } 458 | 459 | /* 460 | Writes out a dictionary to a storage location. That dictionary must be a 'valid' 461 | SecureUDID structure, and the location must be within range. A new location is 462 | created if is didn't already exist. 463 | */ 464 | void SUUIDWriteDictionaryToStorageLocation(NSInteger number, NSDictionary* dictionary) { 465 | UIPasteboard* pasteboard; 466 | 467 | // be sure to respect our limits 468 | if (number < 0 || number >= SUUID_MAX_STORAGE_LOCATIONS) { 469 | return; 470 | } 471 | 472 | // only write out valid structures 473 | if (!SUUIDValidTopLevelObject(dictionary)) { 474 | return; 475 | } 476 | 477 | pasteboard = [UIPasteboard pasteboardWithName:SUUIDPasteboardNameForNumber(number) create:YES]; 478 | if (!pasteboard) { 479 | return; 480 | } 481 | 482 | pasteboard.persistent = YES; 483 | 484 | [pasteboard setData:[NSKeyedArchiver archivedDataWithRootObject:dictionary] 485 | forPasteboardType:SUUIDTypeDataDictionary]; 486 | } 487 | 488 | /* 489 | Clear a storage location, removing anything stored there. Useful for dealing with 490 | potential corruption. Be careful with this function, as it can remove Opt-Out markers. 491 | */ 492 | void SUUIDDeleteStorageLocation(NSInteger number) { 493 | UIPasteboard* pasteboard; 494 | NSString* name; 495 | 496 | if (number < 0 || number >= SUUID_MAX_STORAGE_LOCATIONS) { 497 | return; 498 | } 499 | 500 | name = SUUIDPasteboardNameForNumber(number); 501 | pasteboard = [UIPasteboard pasteboardWithName:name create:NO]; 502 | if (!pasteboard) 503 | return; 504 | 505 | // While setting pasteboard data to nil seems to always remove contents, the 506 | // removePasteboardWithName: call doesn't appear to always work. Using both seems 507 | // like the safest thing to do 508 | [pasteboard setData:nil forPasteboardType:SUUIDTypeDataDictionary]; 509 | [UIPasteboard removePasteboardWithName:name]; 510 | } 511 | 512 | /* 513 | SecureUDID leverages UIPasteboards to persistently store its data. 514 | UIPasteboards marked as 'persistent' have the following attributes: 515 | - They persist across application relaunches, device reboots, and OS upgrades. 516 | - They are destroyed when the application that created them is deleted from the device. 517 | 518 | To protect against the latter case, SecureUDID leverages multiple pasteboards (up to 519 | SUUID_MAX_STORAGE_LOCATIONS), creating one for each distinct domain/app that 520 | leverages the system. The permanence of SecureUDIDs increases exponentially with the 521 | number of apps that use it. 522 | 523 | This function searches for a suitable storage location for a SecureUDID structure. It 524 | attempts to find the structure written by ownerKey. If no owner is found and there are 525 | still open locations, the lowest numbered location is selected. If there are no 526 | available locations, the last-written is selected. 527 | 528 | Once a spot is found, the most-recent data is re-written over this location. The location 529 | is then, finally, returned. 530 | */ 531 | NSInteger SUUIDStorageLocationForOwnerKey(NSData *ownerKey, NSMutableDictionary** ownerDictionary) { 532 | NSInteger ownerIndex; 533 | NSInteger lowestUnusedIndex; 534 | NSInteger oldestUsedIndex; 535 | NSDate* mostRecentDate; 536 | NSDate* oldestUsedDate; 537 | NSDictionary* mostRecentDictionary; 538 | BOOL optedOut; 539 | 540 | ownerIndex = -1; 541 | lowestUnusedIndex = -1; 542 | oldestUsedIndex = 0; // make sure this value is always in range 543 | mostRecentDate = [NSDate distantPast]; 544 | oldestUsedDate = [NSDate distantFuture]; 545 | mostRecentDictionary = nil; 546 | optedOut = NO; 547 | 548 | // The array of SecureUDID pasteboards can be sparse, since any number of 549 | // apps may have been deleted. To find a pasteboard owned by the the current 550 | // domain, iterate all of them. 551 | for (NSInteger i = 0; i < SUUID_MAX_STORAGE_LOCATIONS; ++i) { 552 | NSDate* modifiedDate; 553 | NSDictionary* dictionary; 554 | 555 | dictionary = SUUIDDictionaryForStorageLocation(i); 556 | if (!dictionary) { 557 | if (lowestUnusedIndex == -1) { 558 | lowestUnusedIndex = i; 559 | } 560 | 561 | continue; 562 | } 563 | 564 | // Check the 'modified' timestamp of this pasteboard 565 | modifiedDate = [dictionary valueForKey:SUUIDTimeStampKey]; 566 | optedOut = optedOut || [[dictionary valueForKey:SUUIDOptOutKey] boolValue]; 567 | 568 | // Hold a copy of the data if this is the newest we've found so far. 569 | if ([modifiedDate compare:mostRecentDate] == NSOrderedDescending) { 570 | mostRecentDate = modifiedDate; 571 | mostRecentDictionary = dictionary; 572 | } 573 | 574 | // Check for the oldest entry in the structure, used for eviction 575 | if ([modifiedDate compare:oldestUsedDate] == NSOrderedAscending) { 576 | oldestUsedDate = modifiedDate; 577 | oldestUsedIndex = i; 578 | } 579 | 580 | // Finally, check if this is the pasteboard owned by the requesting domain. 581 | if ([[dictionary objectForKey:SUUIDOwnerKey] isEqual:ownerKey]) { 582 | ownerIndex = i; 583 | } 584 | } 585 | 586 | // If no pasteboard is owned by this domain, establish a new one to increase the 587 | // likelihood of permanence. 588 | if (ownerIndex == -1) { 589 | // Unless there are no available slots, then evict the oldest entry 590 | if ((lowestUnusedIndex < 0) || (lowestUnusedIndex >= SUUID_MAX_STORAGE_LOCATIONS)) { 591 | ownerIndex = oldestUsedIndex; 592 | } else { 593 | ownerIndex = lowestUnusedIndex; 594 | } 595 | } 596 | 597 | // pass back the dictionary, by reference 598 | *ownerDictionary = [NSMutableDictionary dictionaryWithDictionary:mostRecentDictionary]; 599 | 600 | // make sure our Opt-Out flag is consistent 601 | if (optedOut) { 602 | [*ownerDictionary setObject:[NSNumber numberWithBool:YES] forKey:SUUIDOptOutKey]; 603 | } 604 | 605 | // Make sure to write the most recent structure to the new location 606 | SUUIDWriteDictionaryToStorageLocation(ownerIndex, mostRecentDictionary); 607 | 608 | return ownerIndex; 609 | } 610 | 611 | /* 612 | Attempts to validate the full SecureUDID structure. 613 | */ 614 | BOOL SUUIDValidTopLevelObject(id object) { 615 | if (![object isKindOfClass:[NSDictionary class]]) { 616 | return NO; 617 | } 618 | 619 | // Now, we need to verify the current schema. There are a few possible valid states: 620 | // - SUUIDTimeStampKey + SUUIDOwnerKey + at least one additional key that is not SUUIDOptOutKey 621 | // - SUUIDTimeStampKey + SUUIDOwnerKey + SUUIDOptOutKey 622 | 623 | if ([object objectForKey:SUUIDTimeStampKey] && [object objectForKey:SUUIDOwnerKey]) { 624 | NSMutableDictionary* ownersOnlyDictionary; 625 | NSData* ownerField; 626 | 627 | if ([object objectForKey:SUUIDOptOutKey]) { 628 | return YES; 629 | } 630 | 631 | // We have to trust future schema versions. Note that the lack of a schema version key will 632 | // always fail this check, since the first schema version was 1. 633 | if ([[object objectForKey:SUUIDSchemaVersionKey] intValue] > SUUID_SCHEMA_VERSION) { 634 | return YES; 635 | } 636 | 637 | ownerField = [object objectForKey:SUUIDOwnerKey]; 638 | if (![ownerField isKindOfClass:[NSData class]]) { 639 | return NO; 640 | } 641 | 642 | ownersOnlyDictionary = [NSMutableDictionary dictionaryWithDictionary:object]; 643 | 644 | [ownersOnlyDictionary removeObjectForKey:SUUIDTimeStampKey]; 645 | [ownersOnlyDictionary removeObjectForKey:SUUIDOwnerKey]; 646 | [ownersOnlyDictionary removeObjectForKey:SUUIDOptOutKey]; 647 | [ownersOnlyDictionary removeObjectForKey:SUUIDModelHashKey]; 648 | [ownersOnlyDictionary removeObjectForKey:SUUIDSchemaVersionKey]; 649 | 650 | // now, iterate through to verify each internal structure 651 | for (id key in [ownersOnlyDictionary allKeys]) { 652 | if ([key isEqual:SUUIDTimeStampKey] || [key isEqual:SUUIDOwnerKey] || [key isEqual:SUUIDOptOutKey]) 653 | continue; 654 | 655 | if (![key isKindOfClass:[NSData class]]) { 656 | return NO; 657 | } 658 | 659 | if (!SUUIDValidOwnerObject([ownersOnlyDictionary objectForKey:key])) { 660 | return NO; 661 | } 662 | } 663 | 664 | // if all these tests pass, this structure is valid 665 | return YES; 666 | } 667 | 668 | // Maybe just the SUUIDOptOutKey, on its own 669 | if ([[object objectForKey:SUUIDOptOutKey] boolValue] == YES) { 670 | return YES; 671 | } 672 | 673 | return NO; 674 | } 675 | 676 | /* 677 | Attempts to validate the structure for an "owner dictionary". 678 | */ 679 | BOOL SUUIDValidOwnerObject(id object) { 680 | if (![object isKindOfClass:[NSDictionary class]]) { 681 | return NO; 682 | } 683 | 684 | return [object valueForKey:SUUIDLastAccessedKey] && [object valueForKey:SUUIDIdentifierKey]; 685 | } 686 | 687 | @end 688 | --------------------------------------------------------------------------------