I am trying to save a NSDictionary to NSUserDefaults, and am using MD5 hash to check for integrity, using this helpder class: Secure-NSUserDefaults.
The code to set the Dictionary:
#import "NSUserDefaults+MPSecureUserDefaults.h"
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setSecureObject:aDictionary forKey:aKey];
[defaults synchronize];
The code to retrieve it:
BOOL valid = NO;
NSDictionary * aDictionary = [defaults secureDictionaryForKey:aKey valid:&valid];
if (!valid) {
//... hash doesn't match
} else {
//... hash matches
}
This works great as long as the app is running (testing in the simulator right now), but when I exit the simulator and restart the app, the hash value is different than before.
It's as if exiting the app changes the dictionary value (when it's saved to disk perhaps?) in some way. It's not adding visible characters, though, because it looks exactly the same in the debugger.
Would appreciate any ideas from more experienced programmers!
EDIT:
So this seems to work for me. Thoughts?
Change NSUserDefaults+MPSecureUserDefaults.m like so:
- (NSString *)_hashObject:(id)object
{
if (_secretData == nil) {
// Use if statement in case asserts are disabled
NSAssert(NO, #"Provide a secret before using any secure writing or reading methods!");
return nil;
}
// Copy object to make sure it is immutable (thanks Stephen)
object = [object copy];
//added check for array or dictionary
if ([NSJSONSerialization isValidJSONObject:object]) {
NSMutableData *archivedData = [[NSJSONSerialization dataWithJSONObject:object options:0 error:nil] mutableCopy];
[archivedData appendData:_secretData];
if (_deviceIdentifierData != nil) {
[archivedData appendData:_deviceIdentifierData];
}
NSString *hash = [self _hashData:archivedData];
return hash;
}
// Archive & hash
NSMutableData *archivedData = [[NSKeyedArchiver archivedDataWithRootObject:object] mutableCopy];
[archivedData appendData:_secretData];
if (_deviceIdentifierData != nil) {
[archivedData appendData:_deviceIdentifierData];
}
NSString *hash = [self _hashData:archivedData];
////[archivedData release];
return hash;
}
The code you are using, Secure-NSUserDefaults, is incorrect.
The code makes assumption about NSKeyedArchiver's archivedDataWithRootObject: which are invalid - namely that if two dictionaries are the same then the archived version of them is the same. The internal ordering of key/value pairs in a dictionary is not defined, two dictionaries can be semantically the same while being structurally different - and if they are structurally different their archived version of them may be also.
Either write your own or fix the library you have used. You need to deal with dictionaries as an ordered collection of key/values pairs - say by sorting based on the key as NSLog does when printing them.
HTH
Addendum: After question edit
NSJSONSerialization suffers from the same problem (for this usage) as NSKeyedArchiver, as the simple test I posted on GitHub will show.
It seems you may be missing the core problem here. A dictionary is an unordered collection of key/value pairs. The code you are using is attempting to generate a sequence of bytes which is identical (or at least produces the same hash value) for different dictionaries which contain the same key/value pairs in any order. The issue is compounded as dictionaries/arrays can contain other arrays/dictionaries to any nesting depth.
The obvious way to do generate a byte sequence independent of the (internal) ordering is to order the key/values pairs when producing the byte sequence. However dictionary keys are not required to have an ordering, only an equality, relation.
As there is no ordering requirement on keys the NSKeyedArchiver and NSJSONSerialization cannot assume one exists and so do not guarantee to produce the same byte sequence for dictionaries with the same key/value pairs which are ordered (internally to the type) differently. Furthermore NSKeyedArchiver is preserving the object graph, including any sharing, see Object Graphs, which could also contribute to the differences you observe.
However you are writing property lists and for a dictionary to be valid for inclusion in a property list the keys must be strings (see Apple's About Property Lists). Now strings do have an ordering, e.g. NSString's compare: method, so in this particular case you can order the key/value pairs. So you can either write your own code, or find pre-written code, which produces a byte stream for property list types and orders the dictionary key/value pairs while doing so; then you can use this code in the library you are trying to adopt.
Just an idea how this class may be fixed:
NSDictionary should be archived with NSKeyedArchiver not only to calculate hash over it, but also to be saved like that (archived) in the NSUserDefaults (in opposite to the direct storing as it is done now).
In the get method, upon the hash validation, it will be needed additionally to unarchive it with NSKeyedUnarchiver to get back original value.
Thanks.
Related
I've got a Core Data model with 15 versions. It's got code to progressively migrate from the current store's version to the latest version on launch.
Key to that is a call to
NSDictionary* options = #{ NSMigratePersistentStoresAutomaticallyOption : #true,
NSInferMappingModelAutomaticallyOption : #true };
NSDictionary* sourceMetadata = [NSPersistentStoreCoordinator
metadataForPersistentStoreOfType: inType
URL: inSourceStore
options: options
error: outError];
NSManagedObjectModel* model = [NSManagedObjectModel mergedModelFromBundles: #[ [NSBundle bundleForClass: [self class]] ]
forStoreMetadata: inSourceMetadata];
But that's always returning nil, and I'm not sure why. The existing store is version 14, the new model is version 15.
Now, the last change to the model was fairly trivial (the addition of a couple optional fields), so I would have thought it could infer the mapping automatically, but that wasn't working, so I added a mapping model from version 14 to version 15 using Xcode's assistant for that, and made no changes.
Any idea why it's returning nil, or what I can do to investigate this further?
In the same vein, when I say "version 14", I'm referring to the sequential numbering of the .xcdatamodel files. Is there any way to look at the actual store and determine which version of the model Core Data thinks it is?
Well, first of all, you seem like you know what you are doing, having survived through 14 Core Data migrations and all. So I think you should be on the lookout for some silly forehead-slapping type of mistake.
Ensure that [NSBundle bundleForClass: [self class]] is returning the expected bundle, which contains a directory Contents/Resources/YourModelName.momd, and that this directory contains all of the required .mom files (one for each version), and a VersionInfo.plist file. My builds also contain a .omo file for the latest version only.
Now I shall answer your second question, which indeed may help you to answer your first question.
In that VersionInfo.plist file you will find a dictionary named NSManagedObjectModel_VersionHashes, which in turn contains sub-dictionaries, one key for each version. Each version sub-dictionary contains a key for each of your entity names and value which is a 32-byte (256 bit) hash of the attributes and relationships of that entity in that version. Let's call this these the model hashes.
Now open up a store database file with a SQLite viewer, or the sqlite3 command line tool. In that database, alongside one table for each of the entities in the model, you will see a table named Z_METADATA with one row and three columns. The value of the column named Z_PLIST is typed as a blob of binary data. Copy that data to a file, name it with extension .plist, doubleclick and, surprise it opens in your favorite plist editor because that data is in fact a string of text representing an Apple property list in XML format. The value of its key NSStoreModelVersionHashes is in fact a sub-dictionary which is just like the sub-dictionary in the VersionInfo.plist file. Let's call this the store hashes. The 32-byte (256-bit) version hash is Base64 encoded. (There are 44 Base64 characters. Since each Base64 character represents 6 bits, 44 characters can represent up to 44*6 = 264 bits.)
Finally, to answer your second question, the storeMetadata passed to +[NSManagedObjectModel mergedModelFromBundles:forStoreMetadata:] is in fact the Z_METADATA from the store, which contains those store hashes. +[NSManagedObjectModel mergedModelFromBundles:forStoreMetadata:] compares these store hashes to the model hashes from each candidate data model in the passed-in bundle, and returns the model whose model hashes match the store hashes for all entities, with no extra unmatched entities on either side.
So you see it's kind of tedious to do the comparison manually. But probably while spelunking through these plists you will find that forehead-slapper. If not, give us some more context around that code you pasted and probably someone can help.
Ah. It turns out, I edited the latest model version, rather than adding a new one. That's why none would match. Once I reverted the latest version and added a new model version with the changes, it worked fine, even without the default mapping models.
I figured this out by testing each model to see if it matched against the source metadata, using the following:
NSDictionary* storeHashes = [sourceMetadata objectForKey: NSStoreModelVersionHashesKey];
NSArray<NSURL*>* urls = [self getModelURLs];
urls = [urls sortedArrayUsingComparator:
^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2)
{
NSURL* s2 = obj1;
NSURL* s1 = obj2;
return [s1.lastPathComponent compare: s2.lastPathComponent options: NSNumericSearch];
}];
for (NSURL* url in urls)
{
NSDictionary* modelHashes = [self getHashesForModelAtURL: url];
// Compare the hashes…
bool matches = true;
for (NSString* entityKey in storeHashes.allKeys)
{
NSString* storeHash = storeHashes[entityKey];
NSString* modelHash = modelHashes[entityKey];
if (![storeHash isEqual: modelHash])
{
NSLogDebug(#"Model %# has mismatch on %#", url.lastPathComponent, entityKey);
matches = false;
}
}
if (matches)
{
NSLogDebug(#"Version matches: %#", url.lastPathComponent);
break;
}
}
- (NSArray<NSURL*>*)
getModelURLs
{
NSBundle* bundle = [NSBundle bundleForClass: [self class]];
NSArray<NSURL*>* urls = [bundle URLsForResourcesWithExtension: #"mom" subdirectory: #"Model.momd"];
return urls;
}
- (NSDictionary*)
getHashesForModelAtURL: (NSURL*) inURL
{
NSManagedObjectModel* model = [[NSManagedObjectModel alloc] initWithContentsOfURL: inURL];
NSDictionary* hashes = model.entityVersionHashesByName;
return hashes;
}
The following code is returning an exception with the following error message and My Application Crashed, in my code all data store in NSMutableDictionary and then store in NSUserDefaults
After get the data and assign global NSMutableDictionary and i will try to update data in NSMutableDictionary app crash and showing error
-[__NSCFDictionary setObject:forKey:]: mutating method sent to immutable object
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary * cacheUploadDataDic = [[defaults objectForKey:#"SalaryIncomeData"] mutableCopy];
Exactly, it crashes with mutable type issue, all the objects you retrieved from user defaults are inmutable, it uses the plist file to serialized the data list to file, check the plist programming guide about the supported types.
You shall regard the user defaults as a simple plist file storage wrapper.
Use NSMutableDictionary's 'initWithDictionary:' method instead.
Since an answer to the question in my comment does not appear to be forthcoming, I'm just going to go out on a limb and make the guess that your cacheUploadDataDic dictionary contains some more NSDictionary objects inside of it, and that your crash is occurring when you try to mutate one of those dictionaries. This fails because mutableCopy performs a shallow copy; only the dictionary object itself is copied, and all of the objects inside the dictionary, including any additional dictionaries, remain their original immutable selves.
You can fix this by making a deep copy of the dictionary instead. There are a few ways to do this, but the simplest is probably to use the CFPropertyListCreateDeepCopy function. Unfortunately, we have to do some bridging, since this API is only available at the CoreFoundation level, the reason for which is one of those eternal mysteries.
Anyway, do something like this:
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] objectForKey:#"SomeKey"];
NSMutableDictionary *mutableDict = CFBridgingRelease(
CFPropertyListCreateDeepCopy(kCFAllocatorDefault,
(__bridge CFDictionaryRef)dict,
kCFPropertyListMutableContainersAndLeaves)
);
You will now be able to mutate the contents of mutableDict to your heart's content.
Follow your code, there shows no problem.
Then if possible, you can check the class of cacheUploadDataDic before update data and after NSMutableDictionary * cacheUploadDataDic = [[defaults objectForKey:#"SalaryIncomeData"] mutableCopy].
If there are one NSMutableDictionary and the other is NSDictionary then you must change cacheUploadDataDic at other line.
if you can't find out where you change the object,you can try KVO.
I am pulling a JSON object from my API and creating it using the following code (in hindsight not the way to go):
+ (YActivity *)instanceFromDictionary:(NSDictionary *)jsonDictionary
{
YActivity * instance = [[YActivity alloc] init];
[instance setAttributesFromDictionary:jsonDictionary];
return instance;
}
- (void)setAttributesFromDictionary:(NSDictionary *)jsonDictionary
{
if (![jsonDictionary isKindOfClass:[NSDictionary class]]) {
return;
}
[self setValuesForKeysWithDictionary:jsonDictionary];
}
One of the keys is "type". I have a read-only variable #synthesized called "type". On the 32-bit version of my app, this is set right away before setValue:(id)value forUndefinedKey:(NSString *)key is called. I reference this value in that method, and on the 64-bit version of my app, when the breakpoint hits this method, type is not set yet.
Clearly this isn't the best course of action. I am just wondering if anyone else as seen this or if I'm barking up the wrong tree. I diffed the files between the two versions and they are identical. I am running them both on iOS 8.1 Simulator, the API is returning the same thing for both...I'm stumped. Basically on the old version defined keys are set before undefined, and on the new version it seems the opposite of that.
NSDictionary objects are unordered collections, so code should never make assumptions about the order in which a dictionary will enumerate its own keys. It turns out that there are implementation differences between the 32- and 64-bit runtimes that affect where hashed values end up being stored.
Since the API contract explicitly doesn't guarantee order, that shouldn't cause problems, but it can (and in this case apparently does) have the side-effect of causing code that formerly 'worked' to break when compiled for the 64-bit architecture.
A quick way to fix the problem you're currently having without significantly changing the implementation would be to enumerate the dictionary's keys yourself, which would allow you to provide an array of keys ordered however you wish:
- (void)setAttributesFromDictionary:(NSDictionary *)dictionary
{
// So instead of doing this...
//
// [self setValuesForKeysWithDictionary:dictionary];
// You could do something along these lines:
//
NSMutableArray *keys = dictionary.allKeys.mutableCopy;
// TODO: insert code to change the order of the keys array.
// Then loop through the keys yourself...
for (NSString *key in keys)
{
[self setValue:dictionary[key] forKey:key];
}
}
When debugging in XCode, the debugger is telling me that the NSDictionary object contains 1 key/value pair. When the debug console prints the description of the key/value pair is shows:
Printing description of testdictionary:
{
"Unknown (<1809>)" = <000000ff>;
}
I want to extract both the <1809> and the <000000ff>. I have tried both the valueForKey and objectforKey methods as described elsewhere on this site. But I think I am having difficulty understanding what is the key and what is the value here.
For example, is "Unknown (<1809>)" the key? Or is "<1809>" the key? Or is 1809 the key?
Thanks Tim for the reply.
The NSDictionary comes from the CoreBluetoothFramework the didDiscoverPeripheral: method is called and passes advertising data into an NSDictionary called "advertisementData".
This dictionary contains all sorts of stuff like the advertising channel and device name. However, I am trying to extract just the advertising data from "advertisementData". I used the key provided by corebluetooth "CBAdvertisementDataServiceDataKey" like this:
NSData* information;
information = [advertisementData objectForKey:CBAdvertisementDataServiceDataKey];
I was declaring "information" as an NSDictionary* object before. But changed it to NSData* after some more reading on Apples documentation. The result is the same. The debugger says that it contains a key/value pair as follows:
"Unknown (<1809>)" = <000000ff>;
Thanks again.
Nik
When you do not know the keys that are present in the dictionary, for example, because the key-value pairs come from an external source, you can use enumerateKeysAndObjectsUsingBlock: method to go through all key-value pairs present in the dictionary:
[testdictionary enumerateKeysAndObjectsUsingBlock::^(id key, id object, BOOL *stop) {
NSLog(#"The key is %#", key);
NSLog(#"The value is %#", object);
}];
I've never seen this before so this is nothing more than an educated guess:
The dictionary may have been casted from CFDictionaryRef, in which case both the key and value are const void * (instead of NSObject). The key might have been some Core Foundation type holding a file descriptor (hence 1809). The value could be a pointer (or an integer casted to a "pointer": (void *)32).
You should try and find out where the dictionary originates from, because it's the only thing that's going to give you any valuable information.
Update: the docs state that the value of CBAdvertisementDataServiceDataKey is a dictionary. The keys are CBUUID objects, representing CBService UUIDs and the values are NSData objects. (1)
I am pulling data from the web that is formatted in JSON and when I parse the data using "ValueForKeyPath" it stores the string value as an id object.
So I store the data into a NSMutableArray
In the debugger window it shows all the elements added as (id) null.
I have an if statement
if ([[self.activeCategories objectAtIndex:selected] boolValue] == true)
Sometimes I would say 20% of the time it fails the if statement when it should not.
I was wondering if it was because the self.activeCategories is storing id types. Do I need to do [NSString stringWithFormat#"%#", x] to all the objects inside the array? It seems like when I just straight cast it by using (NSString *) it is still type id in the debugger.
It's a very strange error to me... as the error is not consistently reproducible.
Try it like that:
if ([[self.activeCategories objectAtIndex:selected] boolValue])
According to that article a BOOL may hold values other than 0 and 1 which may fail the comparison.