-
Notifications
You must be signed in to change notification settings - Fork 29
/
Copy pathSQLCipherManager.m
1448 lines (1335 loc) · 56 KB
/
SQLCipherManager.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// SQLCipherManager.m
// Strip
//
// Created by Billy Gray on 12/30/09.
// Copyright 2009 Zetetic LLC. All rights reserved.
//
#import "SQLCipherManager.h"
#define kSQLCipherRollback @"rollback"
#define kSQLCipherRekey @"rekey"
#define AES_CBC @"aes-256-cbc"
// database name column for database_list
#define ATTACHED_DATABASE_NAME_COLUMN 1
NSString * const SQLCipherManagerErrorDomain = @"SQLCipherManagerErrorDomain";
NSString * const SQLCipherManagerCommandException = @"SQLCipherManagerCommandException";
NSString * const SQLCipherManagerUserInfoQueryKey = @"SQLCipherManagerUserInfoQueryKey";
@interface SQLCipherManager ()
- (void)sendError:(NSString *)error;
- (NSString *)_nameForKdfAlgo:(PBKDF2_HMAC_ALGORITHM)kdfAlgo;
- (NSString *)_nameForHmacAlgo:(HMAC_ALGORITHM)hmacAlgo;
+ (NSError *)errorWithSQLitePointer:(const char *)errorPointer;
+ (NSError *)errorUsingDatabase:(NSString *)problem reason:(NSString *)dbMessage;
@end
static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
@implementation SQLCipherManager
// needs to be synthesized since we're implementing both the getter and setter (Control is ours!)
@synthesize cachedPassword = _cachedPassword;
@dynamic freeListCount;
@dynamic pageCount;
@dynamic freeListRatio;
static SQLCipherManager *sharedManager = nil;
- (instancetype)init {
self = [super init];
if (self != nil) {
_useHMACPageProtection = YES;
_kdfIterations = -1; // negative one indicates don't modify the kdfIterations from the default of what version of SQLCipher
// set up a serial dispatch queue for database operations
_serialQueue = dispatch_queue_create([[NSString stringWithFormat:@"SQLCipher.%@", self] UTF8String], NULL);
dispatch_queue_set_specific(_serialQueue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
}
return self;
}
- (instancetype)initWithURL:(NSURL *)absoluteUrl {
self = [self init];
if (self != nil) {
_databaseUrl = absoluteUrl;
}
return self;
}
- (instancetype)initWithPath:(NSString *)path {
NSURL *absoluteURL = [[NSURL alloc] initFileURLWithPath:path isDirectory:NO];
return [self initWithURL:absoluteURL];
}
- (void)inQueue:(void (^)(SQLCipherManager *manager))block {
/* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
* and then check it against self to make sure we're not about to deadlock. */
// Credit for this goes to Gus Mueller and his implementation in fmdb/FMDatabaseQueue
SQLCipherManager *currentManager = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
NSAssert(currentManager != self, @"inQueue: was called reentrantly on the same queue, which would lead to a deadlock");
dispatch_sync(self.serialQueue, ^{
@autoreleasepool {
block(self);
}
});
}
- (void)inQueueAsync:(void (^)(SQLCipherManager *manager))block {
/* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
* and then check it against self to make sure we're not about to deadlock. */
// Credit for this goes to Gus Mueller and his implementation in fmdb/FMDatabaseQueue
SQLCipherManager *currentManager = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
NSAssert(currentManager != self, @"inQueue: was called reentrantly on the same queue, which would lead to a deadlock");
dispatch_async(self.serialQueue, ^{
@autoreleasepool {
block(self);
}
});
}
- (void)setDatabasePath:(NSString *)databasePath {
NSURL *url = [[NSURL alloc] initFileURLWithPath:databasePath isDirectory:NO];
[self setDatabaseUrl:url];
}
- (NSString *)databasePath {
return [[self databaseUrl] path];
}
- (NSNumber *)databaseSize {
if (_databaseUrl == nil) {
return nil;
}
NSError *error;
NSFileManager *fm = [NSFileManager defaultManager];
NSDictionary *attrs = [fm attributesOfItemAtPath:[self databasePath] error:&error];
unsigned long long size = [attrs fileSize];
NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong: size];
return fileSize;
}
- (void)sendError:(NSString *)error {
if (self.delegate && [self.delegate respondsToSelector:@selector(didEncounterDatabaseError:)]) {
[self.delegate didEncounterDatabaseError:error];
}
}
+ (NSError *)errorWithSQLitePointer:(const char *)errorPointer {
NSString *errMsg = [NSString stringWithCString:errorPointer encoding:NSUTF8StringEncoding];
NSString *description = @"An error occurred executing a SQL statement";
return [self errorWithDescription:description reason:errMsg];
}
+ (NSError *)errorUsingDatabase:(NSString *)problem reason:(NSString *)dbMessage {
NSString *failureReason = [NSString stringWithFormat:@"DB command failed: '%@'", dbMessage];
return [self errorWithDescription:problem reason:failureReason];
}
+ (NSError *)errorForResultCode:(NSInteger)resultCode {
return [self errorForResultCode:resultCode reason:nil];
}
+ (NSError *)errorForResultCode:(NSInteger)resultCode reason:(NSString * _Nullable)localizedReason {
NSString *description = [NSString localizedStringWithFormat:NSLocalizedString(@"A database error has occurred, result code %li", @"Database error messsage with error code"), resultCode];
NSString *reason = nil;
if (localizedReason != nil) {
reason = localizedReason;
} else {
const char *errorMsgFromRc = sqlite3_errstr((int)resultCode);
if (errorMsgFromRc != NULL) {
reason = [NSString stringWithUTF8String:errorMsgFromRc];
}
}
return [self errorWithDescription:description reason:reason];
}
+ (NSError *)errorWithDescription:(NSString *)localizedDescription reason:(NSString * _Nullable)localizedReason {
NSMutableDictionary *info = [NSMutableDictionary dictionaryWithDictionary:@{NSLocalizedDescriptionKey: localizedDescription}];
if (localizedReason != nil) {
[info setObject:localizedReason forKey:NSLocalizedFailureReasonErrorKey];
}
return [NSError errorWithDomain:SQLCipherManagerErrorDomain
code:ERR_SQLCIPHER_COMMAND_FAILED
userInfo:info];
}
+ (id)sharedManager {
if (sharedManager == nil) {
sharedManager = [[self alloc] init];
}
return sharedManager;
}
+ (void)setSharedManager:(SQLCipherManager *)manager {
sharedManager = manager;
}
+ (void)clearSharedManager {
sharedManager = nil;
}
+ (BOOL)passwordIsValid:(NSString *)password {
if (password == nil) {
return NO;
}
// can't be blank a string, either
if ([[password stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] <= 0) {
return NO;
}
return YES; // all clear!
}
- (NSString *)cachedPassword {
return _cachedPassword;
}
- (void)setCachedPassword:(NSString *)password {
if (_cachedPassword != password) {
NSString *mutableCopy = [password mutableCopy];
if (_cachedPassword != nil) {
memset((void *)[_cachedPassword UTF8String], 0, [_cachedPassword length]);
}
_cachedPassword = mutableCopy;
}
}
# pragma mark -
# pragma mark Open, Create, Re-Key and Close Tasks
- (void)createDatabaseWithPassword:(NSString *)password {
// just a pass-through, really
[self createDatabaseWithPassword:password license:nil];
}
- (void)createDatabaseWithPassword:(NSString *)password license:(NSString *)licenseKey {
[self openDatabaseWithOptions:password cipher:AES_CBC iterations:self.kdfIterations withHMAC:self.useHMACPageProtection license:licenseKey];
}
- (BOOL)openDatabaseWithPassword:(NSString *)password {
return [self openDatabaseWithPassword:password license:nil];
}
- (BOOL)openDatabaseWithPassword:(NSString *)password license:(NSString *)licenseKey {
BOOL unlocked = NO;
unlocked = [self openDatabaseWithOptions:password
cipher:AES_CBC
iterations:self.kdfIterations
withHMAC:self.useHMACPageProtection
license:licenseKey];
return unlocked;
}
- (BOOL)openAndRekeyCFBDatabaseWithPassword:(NSString *)password {
BOOL unlocked = NO;
NSError *error;
NSLog(@"attempting to open in CFB mode, with 4,000 iterations");
unlocked = [self openDatabaseWithOptions: password
cipher: @"aes-256-cfb"
iterations: 4000];
if (unlocked == YES) {
NSLog(@"initiating re-key to new settings");
unlocked = [self rekeyDatabaseWithOptions:password
cipher:AES_CBC
iterations:self.kdfIterations
error:&error];
if (!unlocked && error) {
NSLog(@"error re-keying database: %@", error);
}
}
return unlocked;
}
- (BOOL)openDatabaseWithCachedPassword {
return [self openDatabaseWithCachedPasswordAndLicense:nil];
}
- (BOOL)openDatabaseWithCachedPasswordAndLicense:(NSString *)licenseKey {
return [self openDatabaseWithOptions:self.cachedPassword
cipher:AES_CBC
iterations:self.kdfIterations
withHMAC:YES
license:licenseKey];
}
- (BOOL)openDatabaseWithOptions:(NSString *)password
cipher:(NSString *)cipher
iterations:(NSInteger)iterations {
return [self openDatabaseWithOptions:password
cipher:cipher
iterations:iterations
withHMAC:self.useHMACPageProtection];
}
- (BOOL)openDatabaseWithOptions:(NSString *)password
cipher:(NSString *)cipher
iterations:(NSInteger)iterations
withHMAC:(BOOL)useHMAC {
return [self openDatabaseWithOptions:password
cipher:cipher
iterations:iterations
withHMAC:useHMAC
license:nil];
}
- (BOOL)openDatabaseWithOptions:(NSString *)password
cipher:(NSString *)cipher
iterations:(NSInteger)iterations
withHMAC:(BOOL)useHMAC
license:(NSString *)licenseKey {
// pass through and use default page size of current version of SQLCipher
return [self openDatabaseWithOptions:password
cipher:cipher
iterations:iterations
withHMAC:useHMAC
pageSize:-1
license:licenseKey];
}
- (BOOL)openDatabaseWithOptions:(NSString *)password
cipher:(NSString *)cipher
iterations:(NSInteger)iterations
withHMAC:(BOOL)useHMAC
pageSize:(NSInteger)pageSize
license:(NSString *)licenseKey {
return [self openDatabaseWithOptions:password
cipher:cipher
iterations:iterations
withHMAC:useHMAC
pageSize:pageSize
kdfAlgo:PBKDF2_HMAC_ALGORITHM_DEFAULT
license:licenseKey];
}
- (BOOL)openDatabaseWithOptions:(NSString *)password
cipher:(NSString *)cipher
iterations:(NSInteger)iterations
withHMAC:(BOOL)useHMAC
pageSize:(NSInteger)pageSize
kdfAlgo:(PBKDF2_HMAC_ALGORITHM)kdfAlgo
license:(NSString *)licenseKey {
return [self openDatabaseWithOptions:password
cipher:cipher
iterations:iterations
withHMAC:useHMAC
pageSize:pageSize
kdfAlgo:kdfAlgo
hmacAlgo:HMAC_ALGORITHM_DEFAULT
license:licenseKey];
}
- (BOOL)openDatabaseWithOptions:(NSString *)password
cipher:(NSString *)cipher
iterations:(NSInteger)iterations
withHMAC:(BOOL)useHMAC
pageSize:(NSInteger)pageSize
kdfAlgo:(PBKDF2_HMAC_ALGORITHM)kdfAlgo
hmacAlgo:(HMAC_ALGORITHM)hmacAlgo
license:(NSString *)licenseKey {
BOOL unlocked = NO;
BOOL newDatabase = NO;
if ([self databaseExists] == NO) {
newDatabase = YES;
}
sqlite3 *db = nil;
if (sqlite3_open([[self pathToDatabase] UTF8String], &db) == SQLITE_OK) {
self.database = db;
[self execute:@"PRAGMA foreign_keys = ON;" error:NULL];
// HMAC page protection is enabled by default in SQLCipher 2.0
if (useHMAC == NO) {
[self execute:@"PRAGMA cipher_default_use_hmac = OFF;" error:NULL];
} else {
[self execute:@"PRAGMA cipher_default_use_hmac = ON;" error:NULL];
}
// submit the password
const char *key = [password UTF8String];
sqlite3_key(self.database, key, (int)strlen(key));
// specify the license if one is present
if (licenseKey) {
NSString *licensePragma = [NSString stringWithFormat:@"PRAGMA cipher_license = '%@';", licenseKey];
[self execute:licensePragma];
}
// both cipher and kdf_iter must be specified AFTER key
if (cipher) {
[self execute:[NSString stringWithFormat:@"PRAGMA cipher='%@';", cipher] error:NULL];
}
if (iterations > 0) {
[self execute:[NSString stringWithFormat:@"PRAGMA kdf_iter='%d';", (int)iterations] error:NULL];
}
// check if we have a page size (-1 indicates use the default)
// make sure we're in between valid sqlite page sizes and that we're a power of 2
if (pageSize >= 512 && pageSize <= 65536 && (pageSize & (pageSize -1)) == 0) {
NSString *pageSizePragma = [NSString stringWithFormat:@"PRAGMA cipher_page_size = '%li';", pageSize];
[self execute:pageSizePragma];
} else if (pageSize != -1) { // unless we supplied -1 (use default page size) log an error
NSLog(@">> Invalid Page Size supplied (%li), ignoring", pageSize);
}
// if it's not the default kdf algo, make sure to apply the pragma
NSString *nameForKdfAlgo = [self _nameForKdfAlgo:kdfAlgo];
// we only return non-nill here if it's a valid algo
if (nameForKdfAlgo) {
[self execute:[NSString stringWithFormat:@"PRAGMA cipher_kdf_algorithm = %@;", nameForKdfAlgo]];
}
// now see if an hmac algo is specified
NSString *nameForHmacAlgo = [self _nameForHmacAlgo:hmacAlgo];
if (nameForHmacAlgo) {
[self execute:[NSString stringWithFormat:@"PRAGMA cipher_hmac_algorithm = %@;", nameForHmacAlgo]];
}
unlocked = [self isDatabaseUnlocked];
if (unlocked == NO) {
sqlite3_close(self.database);
} else {
self.cachedPassword = password;
if (newDatabase == YES) {
if (self.delegate && [self.delegate respondsToSelector:@selector(didCreateDatabase:)]) {
[self.delegate didCreateDatabase:self];
}
} else {
if (self.delegate && [self.delegate respondsToSelector:@selector(didOpenDatabase:)]) {
[self.delegate didOpenDatabase:self];
}
}
}
} else {
NSAssert1(0, @"Unable to open database file '%s'", sqlite3_errmsg(self.database));
}
return unlocked;
}
- (NSString *)_nameForKdfAlgo:(PBKDF2_HMAC_ALGORITHM)kdfAlgo {
NSString *name = nil;
switch (kdfAlgo) {
case PBKDF2_HMAC_ALGORITHM_DEFAULT:
// no op, we can just leave the name as nil
break;
case PBKDF2_HMAC_ALGORITHM_SHA1:
name = @"PBKDF2_HMAC_SHA1";
break;
case PBKDF2_HMAC_ALGORITHM_SHA256:
name = @"PBKDF2_HMAC_SHA256";
break;
case PBKDF2_HMAC_ALGORITHM_SHA512:
name = @"PBKDF2_HMAC_SHA512";
break;
}
return name;
}
- (NSString *)_nameForHmacAlgo:(HMAC_ALGORITHM)hmacAlgo {
NSString *name = nil;
switch (hmacAlgo) {
case HMAC_ALGORITHM_DEFAULT:
// no-op leave it as the deafult nil
break;
case HMAC_ALGORITHM_SHA1:
name = @"HMAC_SHA1";
break;
case HMAC_ALGORITHM_SHA256:
name = @"HMAC_SHA256";
break;
case HMAC_ALGORITHM_SHA512:
name = @"HMAC_SHA512";
break;
}
return name;
}
- (BOOL)rekeyDatabaseWithPassword:(NSString *)password {
return [self rekeyDatabaseWithOptions:password cipher:AES_CBC iterations:self.kdfIterations error:NULL];
}
- (BOOL)rekeyDatabaseWithOptions:(NSString *)password
cipher:(NSString *)cipher
iterations:(NSInteger)iterations
error:(NSError **)error {
if (self.delegate && [self.delegate respondsToSelector:@selector(sqlCipherManagerWillRekeyDatabase)])
[self.delegate sqlCipherManagerWillRekeyDatabase];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL failed = NO; // used to track whether any sqlcipher operations have yet failed
// if HMAC page protection should be on (e.g. we're doing an upgrade), make it so:
if (self.useHMACPageProtection) {
[self execute:@"PRAGMA cipher_default_use_hmac = ON;" error:NULL];
} else {
// otherwise, better turn it off for this operation, caller may be looking
// to create another non-HMAC database
[self execute:@"PRAGMA cipher_default_use_hmac = OFF;" error:NULL];
}
// 1. backup current db file
BOOL copied = [self createRollbackDatabase:error];
if (copied == NO) {
NSLog(@"could not create rollback database aborting");
// halt immediatly, can't create a backup
return NO;
}
// make sure there's no older rekey database in the way here
if ([fm fileExistsAtPath: [self pathToRekeyDatabase]]) {
NSLog(@"Removing older rekey database found on disk");
[fm removeItemAtPath:[self pathToRekeyDatabase] error:error];
}
// 2. Attach a re-key database
NSString *sql = nil;
int rc = 0;
// Provide new KEY to ATTACH if not nil
NSError *attachError;
if (password != nil) {
sql = @"ATTACH DATABASE ? AS rekey KEY ?;";
[self execute:sql
error:&attachError
withParams:[NSArray arrayWithObjects:[self pathToRekeyDatabase], password, nil]];
}
else {
// The current key will be used by ATTACH
sql = @"ATTACH DATABASE ? AS rekey;";
[self execute:sql
error:&attachError
withParams:[NSArray arrayWithObjects:[self pathToRekeyDatabase], nil]];
}
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (attachError != nil && error != NULL) {
*error = attachError;
}
}
// 2.a rekey cipher
if (cipher != nil) {
NSLog(@"setting new cipher: %@", cipher);
sql = [NSString stringWithFormat:@"PRAGMA rekey.cipher='%@';", cipher];
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to set rekey.cipher"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// 2.b rekey kdf_iter
if (failed == NO && iterations > 0) {
NSLog(@"setting new kdf_iter: %d", (int)iterations);
sql = [NSString stringWithFormat:@"PRAGMA rekey.kdf_iter='%d';", (int)iterations];
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to set rekey.kdf_iter"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// sqlcipher_export
if (failed == NO && password) {
NSLog(@"exporting schema and data to rekey database");
sql = @"SELECT sqlcipher_export('rekey');";
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to copy data to rekey database"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
// we need to update the user version, too
NSInteger version = self.schemaVersion;
sql = [NSString stringWithFormat:@"PRAGMA rekey.user_version = %d;", (int)version];
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to set user version"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// DETACH rekey database
if (failed == NO) {
sql = @"DETACH DATABASE rekey;";
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to detach rekey database"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// move the new db into place
if (failed == NO) {
// close our current handle to the original db
[self reallyCloseDatabase];
// move the rekey db into place
if ([self restoreDatabaseFromFileAtPath:[self pathToRekeyDatabase] error:error] == NO) {
failed = YES;
}
// test that our new db works
if ([self openDatabaseWithOptions:password
cipher:cipher
iterations:iterations
withHMAC:self.useHMACPageProtection] == NO) {
failed = YES;
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to open database after moving rekey into place"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// if there were no failures...
if (failed == NO) {
// 3.a. remove backup db file, return YES
[fm removeItemAtPath:[self pathToRollbackDatabase] error:nil];
// Remove the rekey db, too, since we copied it over
[fm removeItemAtPath:[self pathToRekeyDatabase] error:nil];
} else { // ah, but there were failures...
// 3.b. close db, replace file with backup
NSLog(@"rekey test failed, restoring db from backup");
[self closeDatabase];
if (![self restoreDatabaseFromRollback:error]) {
NSLog(@"Unable to restore database from backup file");
}
// now this presents an interesting situation... need to let the application/delegate handle this, really
[self.delegate didEncounterRekeyError];
}
// if successful, update cached password
if (failed == NO) {
self.cachedPassword = password;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(sqlCipherManagerDidRekeyDatabase)]) {
[self.delegate sqlCipherManagerDidRekeyDatabase];
}
return (failed) ? NO : YES;
}
- (void)closeDatabase {
sqlite3_close(self.database);
self.inTransaction = NO;
self.database = nil;
}
- (void)reallyCloseDatabase {
if (sqlite3_close(self.database) == SQLITE_BUSY) {
NSLog(@"Warning, database is busy, attempting to interrupt and close...");
// you're not too busy for us, buddy
sqlite3_interrupt(self.database);
sqlite3_close(self.database);
}
self.inTransaction = NO;
self.database = nil;
}
- (BOOL)isDatabaseUnlocked {
if (self.database == nil) {
return NO;
}
@autoreleasepool {
if (sqlite3_exec(self.database, "SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) == SQLITE_OK) {
return YES;
}
}
return NO;
}
- (BOOL)reopenDatabase:(NSError **)error {
[self reallyCloseDatabase];
if ([self openDatabaseWithCachedPassword]) {
return YES;
} else {
if (error != NULL) {
*error = [[self class] errorUsingDatabase:@"Unable to re-open database" reason:@"Unable to open database with cached password"];
}
return NO;
}
}
# pragma mark -
# pragma mark - Raw Key Open/Create/Re-key
- (void)createDatabaseWithRawData:(NSString *)rawHexKey {
[self createDatabaseWithRawData:rawHexKey license:nil];
}
- (void)createDatabaseWithRawData:(NSString *_Nonnull)rawHexKey license:(NSString *_Nullable)licenseKey {
[self openDatabaseWithRawData:rawHexKey cipher:AES_CBC withHMAC:self.useHMACPageProtection license:licenseKey];
}
- (BOOL)openDatabaseWithRawData:(NSString *)rawHexKey {
return [self openDatabaseWithRawData:rawHexKey license:nil];
}
- (BOOL)openDatabaseWithRawData:(NSString *_Nonnull)rawHexKey license:(NSString *_Nullable)licenseKey {
return [self openDatabaseWithRawData:rawHexKey cipher:AES_CBC withHMAC:self.useHMACPageProtection license:licenseKey];
}
- (BOOL)openDatabaseWithRawData:(NSString *)rawHexKey cipher:(NSString *)cipher withHMAC:(BOOL)useHMAC {
return [self openDatabaseWithRawData:rawHexKey cipher:cipher withHMAC:useHMAC license:nil];
}
- (BOOL)openDatabaseWithRawData:(NSString *)rawHexKey cipher:(NSString *)cipher withHMAC:(BOOL)useHMAC license:(NSString *)licenseKey {
BOOL unlocked = NO;
sqlite3 *db = nil;
if (sqlite3_open([[self pathToDatabase] UTF8String], &db) == SQLITE_OK) {
self.database = db;
// HMAC page protection is enabled by default in SQLCipher 2.0
if (useHMAC == NO) {
[self execute:@"PRAGMA cipher_default_use_hmac = OFF;" error:NULL];
} else {
[self execute:@"PRAGMA cipher_default_use_hmac = ON;" error:NULL];
}
// submit the password
if (rawHexKey.length == 64) { // make sure we're at 64 characters
NSString *sqlKey = [NSString stringWithFormat:@"PRAGMA key = \"x'%@'\"", rawHexKey];
[self execute:sqlKey];
}
if (licenseKey) {
NSString *licensePragma = [NSString stringWithFormat:@"PRAGMA cipher_license = '%@';", licenseKey];
[self execute:licensePragma];
}
// both cipher and kdf_iter must be specified AFTER key
if (cipher) {
[self execute:[NSString stringWithFormat:@"PRAGMA cipher='%@';", cipher] error:NULL];
}
unlocked = [self isDatabaseUnlocked];
if (unlocked == NO) {
sqlite3_close(self.database);
} else {
// TODO: make a cached data in place of cached password maybe?
}
} else {
NSAssert1(0, @"Unable to open database file '%s'", sqlite3_errmsg(self.database));
}
return unlocked;
}
- (BOOL)rekeyDatabaseWithRawData:(NSString *)rawHexKey {
return [self rekeyDatabaseRawDataWithOptions:rawHexKey cipher:AES_CBC iterations:self.kdfIterations error:NULL];
}
- (BOOL)rekeyDatabaseRawDataWithOptions:(NSString *)rawHexKey
cipher:(NSString *)cipher
iterations:(NSInteger)iterations
error:(NSError **)error {
NSFileManager *fm = [NSFileManager defaultManager];
BOOL failed = NO; // used to track whether any sqlcipher operations have yet failed
// if HMAC page protection should be on (e.g. we're doing an upgrade), make it so:
if (self.useHMACPageProtection) {
[self execute:@"PRAGMA cipher_default_use_hmac = ON;" error:NULL];
} else {
// otherwise, better turn it off for this operation, caller may be looking
// to create another non-HMAC database
[self execute:@"PRAGMA cipher_default_use_hmac = OFF;" error:NULL];
}
// 1. backup current db file
BOOL copied = [self createRollbackDatabase:error];
if (copied == NO) {
NSLog(@"could not create rollback database aborting");
// halt immediatly, can't create a backup
return NO;
}
// make sure there's no older rekey database in the way here
if ([fm fileExistsAtPath: [self pathToRekeyDatabase]]) {
NSLog(@"Removing older rekey database found on disk");
[fm removeItemAtPath:[self pathToRekeyDatabase] error:error];
}
// 2. Attach a re-key database
NSString *sql = nil;
int rc = 0;
// Provide new KEY to ATTACH if not nil
NSError *attachError;
if (rawHexKey != nil) {
NSString *rawHexKeyWithX = [NSString stringWithFormat:@"%@%@%@", @"x'", rawHexKey, @"'"];
sql = @"ATTACH DATABASE ? AS rekey KEY ?;";
[self execute:sql
error:&attachError
withParams:[NSArray arrayWithObjects:[self pathToRekeyDatabase], rawHexKeyWithX, nil]];
}
else {
// The current key will be used by ATTACH
sql = @"ATTACH DATABASE ? AS rekey;";
[self execute:sql
error:&attachError
withParams:[NSArray arrayWithObjects:[self pathToRekeyDatabase], nil]];
}
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (attachError != nil && error != NULL) {
*error = attachError;
}
}
// 2.a rekey cipher
if (cipher != nil) {
NSLog(@"setting new cipher: %@", cipher);
sql = [NSString stringWithFormat:@"PRAGMA rekey.cipher='%@';", cipher];
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to set rekey.cipher"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// 2.b rekey kdf_iter
if (failed == NO && iterations > 0) {
NSLog(@"setting new kdf_iter: %d", (int)iterations);
sql = [NSString stringWithFormat:@"PRAGMA rekey.kdf_iter='%d';", (int)iterations];
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to set rekey.kdf_iter"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// sqlcipher_export
if (failed == NO && rawHexKey) {
NSLog(@"exporting schema and data to rekey database");
sql = @"SELECT sqlcipher_export('rekey');";
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to copy data to rekey database"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
// we need to update the user version, too
NSInteger version = self.schemaVersion;
sql = [NSString stringWithFormat:@"PRAGMA rekey.user_version = %d;", (int)version];
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to set user version"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// DETACH rekey database
if (failed == NO) {
sql = @"DETACH DATABASE rekey;";
rc = sqlite3_exec(self.database, [sql UTF8String], NULL, NULL, NULL);
if (rc != SQLITE_OK) {
failed = YES;
// setup the error object
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to detach rekey database"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// move the new db into place
if (failed == NO) {
// close our current handle to the original db
[self reallyCloseDatabase];
// move the rekey db into place
if ([self restoreDatabaseFromFileAtPath:[self pathToRekeyDatabase] error:error] == NO) {
failed = YES;
}
// test that our new db works
if ([self openDatabaseWithRawData:rawHexKey cipher:cipher withHMAC:self.useHMACPageProtection] == NO) {
failed = YES;
if (error != NULL) {
*error = [SQLCipherManager errorUsingDatabase:@"Unable to open database after moving rekey into place"
reason:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]];
}
}
}
// if there were no failures...
if (failed == NO) {
// 3.a. remove backup db file, return YES
[fm removeItemAtPath:[self pathToRollbackDatabase] error:nil];
// Remove the rekey db, too, since we copied it over
[fm removeItemAtPath:[self pathToRekeyDatabase] error:nil];
} else { // ah, but there were failures...
// 3.b. close db, replace file with backup
NSLog(@"rekey test failed, restoring db from backup");
[self closeDatabase];
if (![self restoreDatabaseFromRollback:error]) {
NSLog(@"Unable to restore database from backup file");
}
// now this presents an interesting situation... need to let the application/delegate handle this, really
[self.delegate didEncounterRekeyError];
}
// if successful, update cached password
if (failed == NO) {
self.cachedPassword = rawHexKey;
}
return (failed) ? NO : YES;
}
# pragma mark -
# pragma mark Backup and file location methods
- (NSString *)databaseDirectory {
// pass back the parent directory of the user-specified databasePath
return [[self databasePath] stringByDeletingLastPathComponent];
}
- (BOOL)databaseExists {
BOOL exists = NO;
// this method just returns YES in iOS, is not implemented
NSError *error = nil;
exists = [[self databaseUrl] checkResourceIsReachableAndReturnError:&error];
return exists;
}
- (NSString *)pathToDatabase {
return [self databasePath];
}
- (NSString *)pathToRollbackDatabase {
return [[self databasePath] stringByAppendingPathExtension:kSQLCipherRollback];
}
- (NSString *)pathToRekeyDatabase {
return [[self databasePath] stringByAppendingPathExtension:kSQLCipherRekey];
}
- (BOOL)restoreDatabaseFromRollback:(NSError **)error {
BOOL success = [self restoreDatabaseFromFileAtPath:[self pathToRollbackDatabase] error:error];
if (success) {
success = [self removeRollbackDatabase:error];
}
return success;
}
- (BOOL)removeRollbackDatabase:(NSError **)error {
NSFileManager *fm = [NSFileManager defaultManager];
return [fm removeItemAtPath:[self pathToRollbackDatabase] error:error];
}
- (BOOL)restoreDatabaseFromFileAtPath:(NSString *)path error:(NSError **)error {
return [self restoreDatabaseFromFileAtPath:path error:error requiresMainDbFilePresent:YES];
}
- (BOOL)restoreDatabaseFromFileAtPath:(NSString *)path error:(NSError *_Nullable*_Nullable)error requiresMainDbFilePresent:(BOOL)requiresMainDbPresent {
BOOL success = NO;
NSFileManager *fm = [NSFileManager defaultManager];
// get the db paths
NSString *dbPath = [self pathToDatabase];
BOOL mainDbFileExists = [fm fileExistsAtPath:dbPath];
NSString *backupPath = path; // argument from caller should be full path to file
// insist that the two files be present
// only insist that the main db file is present if our flag is set to YES
if (mainDbFileExists == NO && requiresMainDbPresent) {
if (error != NULL) {
*error = [[self class] errorUsingDatabase:@"Unable to restore from rollback database" reason:@"Missing file to replace"];
}
return success;
}
if ([fm fileExistsAtPath:backupPath] == NO) {
if (error != NULL) {
*error = [[self class] errorUsingDatabase:@"Unable to restore from rollback database" reason:@"Missing rollback database file"];
}
return success;
}
if (mainDbFileExists) {
// first create a temporary copy of the database file in case our move fails
NSString *tempPath = [dbPath stringByAppendingString:@".temp"];
if ([fm copyItemAtPath:dbPath toPath:tempPath error:error]) {
// remove the original to make way for the backup
NSLog(@"removing the file at the primary database path...");
if ([fm removeItemAtPath:dbPath error:error]) {
// now move the backup to the original location
NSLog(@"moving the backup file into the primary database path...");
if ([fm copyItemAtPath:backupPath toPath:dbPath error:error]) {
success = YES;
}
}
}
if (success == NO) {
// only move the temp db into place if the database removal was successful
if ([fm fileExistsAtPath:dbPath] == NO) {
// we don't want to grab the error from here, as we want to report the error from above
[fm copyItemAtPath:tempPath toPath:dbPath error:nil];
}
}
// we don't want to grab the error from here, as we want to report the error from above
[fm removeItemAtPath:tempPath error:nil];
} else {
// no main db file exists, just move the backup into place
NSLog(@"moving the backup file into the primary database path...");
if ([fm copyItemAtPath:backupPath toPath:dbPath error:error]) {
success = YES;
}
}
return success;
}
- (BOOL)createReplicaAtPath:(NSString *)path {
BOOL success = NO;
sqlite3 *replica = nil;
if (sqlite3_open([path UTF8String], &replica) == SQLITE_OK) {
// initialize it with the cached password
const char *key = [self.cachedPassword UTF8String];
sqlite3_key(replica, key, (int)strlen(key));
// do a quick check to make sure it took
if (sqlite3_exec(replica, "SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) == SQLITE_OK) {
success = YES;
}
}
else {
NSAssert1(0, @"Failed to create replica '%s'", sqlite3_errmsg(replica));
}
return success;
}
- (BOOL)createRollbackDatabase:(NSError **)error {
return [self copyDatabaseToPath:[self pathToRollbackDatabase] error:error];
}
- (BOOL)copyDatabaseToPath:(NSString *)path error:(NSError **)error {
NSFileManager *fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:path]) {