pEpObjCAdapter/PEPPassphraseCache.m
author Dirk Zimmermann <dz@pep.security>
Mon, 29 Jun 2020 16:39:43 +0200
branchIOSAD-172
changeset 1541 c6f4eab1ed95
parent 1540 61a8e9a60a4c
child 1547 516474ecf260
permissions -rw-r--r--
IOSAD-172 Sort passphrases on changes
     1 //
     2 //  PEPPassphraseCache.m
     3 //  pEpObjCAdapter
     4 //
     5 //  Created by Dirk Zimmermann on 25.06.20.
     6 //  Copyright © 2020 p≡p. All rights reserved.
     7 //
     8 
     9 #import "PEPPassphraseCache.h"
    10 
    11 #import "PEPPassphraseCacheInternal.h"
    12 
    13 #import "PEPPassphraseCacheEntry.h"
    14 
    15 static NSUInteger s_maxNumberOfPassphrases = 20;
    16 static NSTimeInterval s_defaultTimeoutInSeconds = 10 * 60;
    17 static NSTimeInterval s_defaultCheckExpiryInterval = 60;
    18 
    19 @interface PEPPassphraseCache ()
    20 
    21 /// Timeout of passwords in seconds.
    22 @property (nonatomic) NSTimeInterval timeout;
    23 
    24 @property (nonatomic) dispatch_queue_t queue;
    25 @property (nonatomic) NSMutableArray<PEPPassphraseCacheEntry *> *mutablePassphraseEntries;
    26 @property (nonatomic) dispatch_source_t timer;
    27 
    28 @end
    29 
    30 @implementation PEPPassphraseCache
    31 
    32 static PEPPassphraseCache *s_sharedInstance;
    33 
    34 + (void)initialize
    35 {
    36     static BOOL initialized = NO;
    37     if (!initialized) {
    38         initialized = YES;
    39         s_sharedInstance = [[PEPPassphraseCache alloc] init];
    40     }
    41 }
    42 
    43 + (instancetype)sharedInstance
    44 {
    45     return s_sharedInstance;
    46 }
    47 
    48 /// Internal constructor (for now).
    49 - (instancetype)initWithPassphraseTimeout:(NSTimeInterval)timeout
    50                       checkExpiryInterval:(NSTimeInterval)checkExpiryInterval
    51 {
    52     self = [super init];
    53     if (self) {
    54         _timeout = timeout;
    55         _queue = dispatch_queue_create("PEPPassphraseCache Queue", DISPATCH_QUEUE_SERIAL);
    56         _mutablePassphraseEntries = [NSMutableArray arrayWithCapacity:s_maxNumberOfPassphrases];
    57 
    58         // we have a strong reference to the timer, but the timer doesn't have one to us
    59         typeof(self) __weak weakSelf = self;
    60 
    61         _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
    62         dispatch_source_set_timer(_timer,
    63                                   DISPATCH_TIME_NOW,
    64                                   checkExpiryInterval * NSEC_PER_SEC,
    65                                   checkExpiryInterval / 10 * NSEC_PER_SEC);
    66         dispatch_source_set_event_handler(_timer, ^{
    67             [weakSelf removeStaleEntries];
    68         });
    69         dispatch_resume(_timer);
    70     }
    71     return self;
    72 }
    73 
    74 /// Public constructor with default values.
    75 - (instancetype)init
    76 {
    77     return [self initWithPassphraseTimeout:s_defaultTimeoutInSeconds
    78                        checkExpiryInterval:s_defaultCheckExpiryInterval];
    79 }
    80 
    81 - (void)addPassphrase:(NSString *)passphrase
    82 {
    83     PEPPassphraseCacheEntry *entry = [[PEPPassphraseCacheEntry alloc]
    84                                       initWithPassphrase:passphrase];
    85     dispatch_sync(self.queue, ^{
    86         [self.mutablePassphraseEntries addObject:entry];
    87         if (self.mutablePassphraseEntries.count > s_maxNumberOfPassphrases) {
    88             [self.mutablePassphraseEntries removeObjectAtIndex:0];
    89         }
    90 
    91         [self sortPassphrases];
    92     });
    93 }
    94 
    95 - (NSArray<NSString *> *)passphrases
    96 {
    97     NSMutableArray *resultingPassphrases = [NSMutableArray
    98                                             arrayWithCapacity:s_maxNumberOfPassphrases + 1];
    99     dispatch_sync(self.queue, ^{
   100         for (PEPPassphraseCacheEntry *entry in self.mutablePassphraseEntries) {
   101             if (![self isExpiredPassphraseEntry:entry]) {
   102                 [resultingPassphrases addObject:entry.passphrase];
   103             }
   104         }
   105     });
   106     [resultingPassphrases insertObject:@"" atIndex:0];
   107     return [NSArray arrayWithArray:resultingPassphrases];
   108 }
   109 
   110 /// Remove password entries that have timed out.
   111 /// - Note: Assumes it gets called on `queue`.
   112 - (void)removeStaleEntries
   113 {
   114     NSMutableArray *resultingPassphrases = [NSMutableArray
   115                                             arrayWithCapacity:s_maxNumberOfPassphrases];
   116 
   117     for (PEPPassphraseCacheEntry *entry in self.mutablePassphraseEntries) {
   118         if (![self isExpiredPassphraseEntry:entry]) {
   119             [resultingPassphrases addObject:entry];
   120         }
   121     }
   122 
   123     [self.mutablePassphraseEntries removeAllObjects];
   124     [self.mutablePassphraseEntries addObjectsFromArray:resultingPassphrases];
   125 }
   126 
   127 - (void)resetTimeoutForPassphrase:(NSString *)passphrase
   128 {
   129     if ([passphrase isEqualToString:@""]) {
   130         // Ignore the empty passphrase, it's always there from the client's view,
   131         // but not contained in the internal model.
   132         return;
   133     }
   134 
   135     dispatch_sync(self.queue, ^{
   136         BOOL foundAtLeastOnce = NO;
   137         for (PEPPassphraseCacheEntry *entry in self.mutablePassphraseEntries) {
   138             if ([entry.passphrase isEqualToString:passphrase]) {
   139                 foundAtLeastOnce = YES;
   140                 entry.dateAdded = [NSDate date];
   141             }
   142         }
   143 
   144         if (foundAtLeastOnce) {
   145             [self sortPassphrases];
   146         }
   147     });
   148 }
   149 
   150 - (BOOL)isExpiredPassphraseEntry:(PEPPassphraseCacheEntry *)passphraseEntry
   151 {
   152     NSDate *now = [NSDate date];
   153     NSDate *minimum = [now dateByAddingTimeInterval:-self.timeout];
   154     NSTimeInterval minimumTimeInterval = [minimum timeIntervalSinceReferenceDate];
   155 
   156     if ([passphraseEntry.dateAdded timeIntervalSinceReferenceDate] < minimumTimeInterval) {
   157         return YES;
   158     }
   159 
   160     return NO;
   161 }
   162 
   163 /// Sort the stored passphrases, last (successfully) used or added first.
   164 /// Assumes being called from the internal queue.
   165 - (void)sortPassphrases
   166 {
   167     NSArray *sorted = [self sortedArrayByDateNewestFirst:self.mutablePassphraseEntries];
   168     [self.mutablePassphraseEntries
   169      replaceObjectsInRange:NSMakeRange(0, [self.mutablePassphraseEntries count])
   170      withObjectsFromArray:sorted];
   171 }
   172 
   173 - (NSArray<PEPPassphraseCacheEntry *> *)sortedArrayByDateNewestFirst:(NSArray<PEPPassphraseCacheEntry *> *)array
   174 {
   175     return [array sortedArrayUsingComparator:^NSComparisonResult(PEPPassphraseCacheEntry *entry1,
   176                                                                  PEPPassphraseCacheEntry *entry2) {
   177         NSTimeInterval interval1 = [entry1.dateAdded timeIntervalSinceReferenceDate];
   178         NSTimeInterval interval2 = [entry2.dateAdded timeIntervalSinceReferenceDate];
   179 
   180         if (interval1 > interval2) {
   181             return NSOrderedAscending;
   182         } else if (interval1 < interval2) {
   183             return NSOrderedDescending;
   184         } else {
   185             return NSOrderedSame;
   186         }
   187     }];
   188 }
   189 
   190 @end