iOS開源加密相冊Agony的實(shí)現(xiàn)(一)

簡介

雖然目前市面上有一些不錯(cuò)的加密相冊App,但不是內(nèi)置廣告,就是對上傳的張數(shù)有所限制。本文介紹了一個(gè)加密相冊的制作過程,該加密相冊將包括多密碼(輸入不同的密碼即可訪問不同的空間,可掩人耳目)、WiFi傳圖、照片文件加密等功能。目前項(xiàng)目和文章會(huì)同時(shí)前進(jìn),項(xiàng)目的源代碼可以在github上下載。
點(diǎn)擊前往GitHub

概述

本文主要介紹加密相冊的登錄驗(yàn)證與注冊模塊的實(shí)現(xiàn)。注冊時(shí)只需要密碼,每個(gè)密碼對應(yīng)一個(gè)獨(dú)立的存儲(chǔ)空間,登錄時(shí)通過Touch ID或密碼驗(yàn)證。如果有多套密碼,Touch ID會(huì)被綁定到一個(gè)主密碼上(可更改)。

賬戶數(shù)據(jù)存儲(chǔ)設(shè)計(jì)

賬戶類設(shè)計(jì)

由于加密相冊只用于本地,當(dāng)前設(shè)計(jì)還未考慮密碼找回,因此賬戶只需要密碼這一字段即可,為了統(tǒng)計(jì)當(dāng)前已有賬戶數(shù)量,再使用一個(gè)id字段,賬戶類SGAccount設(shè)計(jì)如下。

@interface SGAccount : NSObject <NSSecureCoding>

@property (nonatomic, assign) NSInteger accountId;
@property (nonatomic, copy) NSString *password;

@end

為了進(jìn)行歸檔存儲(chǔ),需要實(shí)現(xiàn)NSCoding的相關(guān)方法,如下。

#import "SGAccount.h"

NSString * const kSGAccountId = @"kSGAccountId";
NSString * const kSGAccountPwd = @"kSGAccountPwd";

@implementation SGAccount

- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeInteger:self.accountId forKey:kSGAccountId];
    [encoder encodeObject:self.password forKey:kSGAccountPwd];
}

- (instancetype)initWithCoder:(NSCoder *)decoder {
    if (self = [super init]) {
        self.accountId = [decoder decodeIntegerForKey:kSGAccountId];
        self.password = [decoder decodeObjectForKey:kSGAccountPwd];
    }
    return self;
}

+ (BOOL)supportsSecureCoding {
    return YES;
}

@end

賬戶集合類設(shè)計(jì)

對于多個(gè)賬戶,使用一個(gè)賬戶集合類來管理,賬戶集合類管理所有的賬戶,由于登錄驗(yàn)證時(shí)需要查詢密碼對應(yīng)的賬戶是否存在,為了高效查找,應(yīng)該使用以密碼為key的Map,也就是NSDictionary來存儲(chǔ)。
除此之外,還需要記錄Touch ID對應(yīng)的密碼,綜上所述,設(shè)計(jì)如下。

@interface SGAccountSet : NSObject <NSSecureCoding>

@property (nonatomic, strong) NSMutableDictionary<NSString *, SGAccount *> *accountMap;
@property (nonatomic, copy) NSString *touchIDPassword;

@end

同理這些屬性也需要在NSCoding的相關(guān)方法里處理,類的實(shí)現(xiàn)如下。

#import "SGAccountSet.h"

NSString * const kSGAccountSetAccountMap = @"kSGAccountSetAccountMap";
NSString * const kSGAccountSetTouchIDPassword = @"kSGAccountSetTouchIDPassword";

@implementation SGAccountSet

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)initWithCoder:(NSCoder *)decoder {
    if (self = [super init]) {
        self.accountMap = [decoder decodeObjectForKey:kSGAccountSetAccountMap];
        self.touchIDPassword = [decoder decodeObjectForKey:kSGAccountSetTouchIDPassword];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.accountMap forKey:kSGAccountSetAccountMap];
    [encoder encodeObject:self.touchIDPassword forKey:kSGAccountSetTouchIDPassword];
}

- (NSMutableDictionary<NSString *,SGAccount *> *)accountMap {
    if (_accountMap == nil) {
        _accountMap = @{}.mutableCopy;
    }
    return _accountMap;
}

@end

對于accountMap的懶加載,可以保證在沒有賬戶數(shù)據(jù)時(shí)拿到的字典不為空。

賬戶管理類的設(shè)計(jì)

公有接口設(shè)計(jì)

賬戶管理類對外提供的接口主要是注冊與驗(yàn)證,為了方便,作為單例使用。
注冊時(shí)只需提供密碼即可,而驗(yàn)證包括兩種情況,其一是通過密碼驗(yàn)證,第二是通過Touch ID驗(yàn)證,當(dāng)驗(yàn)證成功時(shí)直接返回賬戶類。
除此之外,賬戶管理類還有一個(gè)屬性currentAccount記錄當(dāng)前驗(yàn)證成功的賬戶,以便后續(xù)使用,具體設(shè)計(jì)如下。

@interface SGAccountManager : NSObject

+ (instancetype)sharedManager;
- (void)registerAccountWithPassword:(NSString *)password errorMessage:(NSString * __autoreleasing *)errorMessage;
- (SGAccount *)getAccountByPwd:(NSString *)pwd;
- (SGAccount *)getTouchIDAccount;
/*
 *  用于AppDelegate獲取窗口的根控制器
 *  沒有注冊過賬戶則進(jìn)入注冊頁面
 *  注冊過用戶則進(jìn)入登錄驗(yàn)證頁面
 */
- (UIViewController *)getRootViewController;

@property (nonatomic, strong) SGAccount *currentAccount;

@end

私有接口設(shè)計(jì)

私有接口用于管理類內(nèi)部的邏輯實(shí)現(xiàn),其中accountSet用于存儲(chǔ)所有用戶數(shù)據(jù),accountPath用于存儲(chǔ)賬戶數(shù)據(jù)保存和加載的路徑。

@interface SGAccountManager ()

@property (nonatomic, strong) SGAccountSet *accountSet;
@property (nonatomic, copy) NSString *accountPath;

@end

賬戶集合accountSet的懶加載
賬戶集合類的初始化包括兩個(gè)步驟,首先從硬盤加載數(shù)據(jù),如果硬盤上沒有數(shù)據(jù),則初始化一個(gè)。之所以分解為兩個(gè)方法,是因?yàn)閺挠脖P加載數(shù)據(jù)的方法loadAccountSet會(huì)被在其他地方調(diào)用,實(shí)現(xiàn)如下。

- (SGAccountSet *)accountSet {
    if (_accountSet == nil) {
        [self loadAccountSet];
    }
    return _accountSet;
}
- (void)loadAccountSet {
    SGAccountSet *set = [NSKeyedUnarchiver unarchiveObjectWithFile:self.accountPath];
    if (!set) {
        set = [SGAccountSet new];
    }
    _accountSet = set;
}

賬戶存取路徑accountPath的懶加載
賬戶數(shù)據(jù)的存儲(chǔ)路徑會(huì)在加載和寫入賬戶集合類數(shù)據(jù)時(shí)使用,實(shí)現(xiàn)如下。

- (NSString *)accountPath {
    if (_accountPath == nil) {
        _accountPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"account.agony"];
    }
    return _accountPath;
}

注冊的實(shí)現(xiàn)

注冊時(shí)傳入密碼,密碼經(jīng)過加密后,先判斷賬戶集合中是否已經(jīng)存在此密碼,以防止密碼重復(fù),這是因?yàn)槊艽a與存儲(chǔ)空間一一對應(yīng),因此密碼不能重復(fù)。如果密碼重復(fù),則通過傳入的字符串指針回傳。
對于第一次注冊的密碼,將會(huì)被綁定到Touch ID上,以后使用Touch ID驗(yàn)證時(shí)則相當(dāng)于輸入此密碼,注冊方法的實(shí)現(xiàn)如下。

- (void)registerAccountWithPassword:(NSString *)password errorMessage:(NSString * __autoreleasing *)errorMessage {
    NSAssert(password != nil, @"password cannot be nil");
    // 對密碼進(jìn)行MD5+鹽的加密處理
    password = [self encryptString:password];
    SGAccount *account = self.accountSet.accountMap[password];
    // 如果根據(jù)要注冊的密碼能取到賬戶,則說明密碼重復(fù),回傳錯(cuò)誤并返回
    if (account != nil) {
        *errorMessage = @"Account Already Exists";
        return;
    }
    account = [SGAccount new];
    // 生成賬戶id
    NSInteger accountid = self.accountSet.accountMap.allKeys.count + 1;
    account.accountId = accountid;
    account.password = password;
    // 存入到集合中
    self.accountSet.accountMap[password] = account;
    if (accountid == 1) {
        // 如果是第一次注冊,則將其綁定到Touch ID驗(yàn)證對應(yīng)的密碼上
        self.accountSet.touchIDPassword = password;
    }
    // 將內(nèi)存數(shù)據(jù)同步到硬盤
    [self saveAccountSet];
}

加密方法的實(shí)現(xiàn)如下。

- (NSString *)encryptString:(NSString *)string {
    return [[[[NSString stringWithFormat:@"allowsad12345%@62232",string] MD5] MD5] MD5];
}

MD5方法通過分類的形式添加到NSString上,實(shí)現(xiàn)如下。

#import "NSString+MD5.h"
#import <CommonCrypto/CommonDigest.h>

@implementation NSString (MD5)

- (NSString *)MD5 {
    const char *cStr = [self UTF8String];
    unsigned char digest[CC_MD5_DIGEST_LENGTH];
    CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
    NSMutableString *result = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [result appendFormat:@"%02x",digest[i]];
    }
    return result;
}

@end

將數(shù)據(jù)寫入到硬盤的方法實(shí)現(xiàn)如下。

- (void)saveAccountSet {
    [NSKeyedArchiver archiveRootObject:self.accountSet toFile:self.accountPath];
}

登錄驗(yàn)證的實(shí)現(xiàn)

通過密碼驗(yàn)證的方式,先將密碼加密,再與集合中的密碼比對,找到匹配的則驗(yàn)證成功,實(shí)現(xiàn)如下。

- (SGAccount *)getAccountByPwd:(NSString *)pwd {
    pwd = [self encryptString:pwd];
    return self.accountSet.accountMap[pwd];
}

通過Touch ID驗(yàn)證的方式,需要在Touch ID驗(yàn)證成功后調(diào)用,使用Touch ID對應(yīng)的密碼進(jìn)行驗(yàn)證,實(shí)現(xiàn)如下。

- (SGAccount *)getTouchIDAccount {
    NSString *pwd = self.accountSet.touchIDPassword;
    return self.accountSet.accountMap[pwd];
}

窗口根控制器選擇的實(shí)現(xiàn)

如果已經(jīng)有了賬戶,則返回導(dǎo)航控制器包裹的驗(yàn)證控制器SGWelcomeViewController,如果沒有注冊過賬戶,則先初始化一個(gè)導(dǎo)航控制器包裹的SGWelcomeViewController,并且向視圖棧中push一個(gè)注冊控制器SGRegisterViewController,之所以這么做,是為了保證注冊完成后能夠返回到驗(yàn)證控制器,并與從驗(yàn)證頁面進(jìn)入的注冊保持相同的邏輯,具體實(shí)現(xiàn)如下。

- (UIViewController *)getRootViewController {
    if ([self hasAccount]) {
        return [[UINavigationController alloc] initWithRootViewController:[SGWelcomeViewController new]];
    }
    SGWelcomeViewController *welcomeVc = [SGWelcomeViewController new];
    SGRegisterViewController *registerVc = [SGRegisterViewController new];
    UINavigationController *nav = [UINavigationController new];
    nav.viewControllers = @[welcomeVc, registerVc];
    return nav;
}

總結(jié)

本文主要介紹了與注冊與登錄驗(yàn)證有關(guān)的數(shù)據(jù)類和管理類的接口與實(shí)現(xiàn)過程,在后面的注冊與登錄驗(yàn)證視圖設(shè)計(jì)中,只需要使用工具類即可。歡迎關(guān)注項(xiàng)目后續(xù),項(xiàng)目的下載地址在本文的開頭可以找到。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容