iOS 用密钥对数据加密解密

╄→尐↘猪︶ㄣ 提交于 2020-03-11 03:09:14

引言

在iOS App开发中,我们需要对账户,密码等个人私密信息进行加密处理,从而保证用户信息的安全。那么可以将这些私密信息保存到钥匙串(keychain)中,因为钥匙串的不可见性,可以保证用户私密信息的安全。
值得注意的是,将明文存入钥匙串中是不安全的。因此可以将用户私密信息通过算法加密后再存进钥匙串中,这样就更进一步的保证了用户的信息安全。

常用的加密算法

加密算法 = 对称性加密算法 + 非对称性加密算法

说明:加密算法通常分为对称加密算法和非对称假面算法。

一、 对称加密算法:

  1. 原理:信息接收双方都需要事先知道密钥和加密解密算法,并且这个密钥是相同的,之后就可以对数据进行加密解密了。
  2. 常见的有:AES,DES,3DES:
  • AES(Advanced Encryption Standard): 高级加密标准,是下一代的加密算法标准,速度快,安全级别高。
  • DES(Data Encryption Standard): 数据加密标准,速度较快,适用加密大量的数据。
  • 3DES(Triple DES):是基于DES,对一块数据用三种不同的密钥进行三次加密,强度较DES更高。

二、 非对称性加密算法:

  1. 原理:通信双方A,B事先生成一个密钥对(私钥+公钥),然后A将自己的公钥发送给B,B也将自己的公钥发送给A,即通信双方将各自的公钥做一个交换。如果A要给B发送一条秘密电报,就需要用B给的公钥对电报进行加密,然后发送给B。B接收到这个秘密电报后,要想A给他传达了什么信息,就需要B用自己的私钥对电报进行解密。B向A发送消息时也是同样的道理。
  2. 常见的有:RSA,DSA,ECC。
  • RSA:是有RSA公司提出的。支持长数据加密。
  • DSA(Digital Signature Algorithm):数据签名算法,是一套数字签名保准(DSS)。
  • ECC(Elliptic Curves Cryptography):椭圆曲线密码编码学。

私钥和密钥:非对称性加密算法中提到私钥和公钥的概念。其作用可描述为:用公钥加密的内容只能用私钥解密,用私钥加密的内容也只能用公钥解密。一般是,公钥加密私钥解密,准确的说应该是“私钥签名,公钥验证”。

线性离散算法 (非加解密算法)##

如MD5,SHA1,HMAC等就属于线性离散算法。这几种算法只是生成一串不可逆的密文,经常用于校检数据在传输过程中是否被篡改过。若使用相同的算法,却生成了不同的密文,那么可以说明数据在传输过程中被修改过。

哈希函数,如MD5,SHA这些都不是加密算法,有很多人称他们为加密算法,这是不正确的。这些算法没有密钥的概念,类似于指纹,是不可逆的。MD5是128位的,SHA有128,256等位,如SHA128,SHA256,SHA384。我们常用的Base64就更不属于加密算法的范畴了,它只是通过Base64编码,将一些加密后的密文中一些特殊的byte[]数组显式的显示出来,转换后,长度会增加,是个可逆的过程。

加密算法的选择

  • 非对称加密算法的运行速度比对对称加密算法的速度慢很多,当我们需要加密大量数据时,通常采用对称加密算法,以此来提高加密解密速度。
  • 对称加密算法不能实现数字签名,需要签名只能使用非对称算法。

折中办法:

  • 由于对称加密算法的密钥管理是一个复杂的过程,密钥的管理直接决定了数据的安全性,因此当数据量很小时,我们通常采用的是非对称加密算法。
  • 我们在实际操作过程中也是这样做的,通常采用非对称加密算法管理对称算法的密钥,然后用对称加密算法来加密数据。
  • 这样一来就集成了两类加密算法的优点,既提升了加密解密的速度,也能安全方便的管理密钥。

密钥管理

你可以使用公钥和私钥来进行非对称加密,而使用对称密钥来进行对称加密。

当你从受密码保护的文件或钥匙串中验证身份时,可以提取其包含的私钥。拥有私钥后,也可以使用SecKeyCopyPublicKey(_:)提取公钥。这就是为什么我们通常把私钥存进钥匙串中,对公钥不做保存。

一、 生成密钥,保存私钥

Keeping a private key in a keychain is a great way to secure it. The key data is encrypted on disk and accessible only to your app or the apps you authorize. However, to use the key, you do eventually copy a plain-text version of it into system memory, even if briefly. Though this presents a reasonably small attack surface, there is still the chance that if your app is compromised, the key could become compromised as well. As an added layer of protection, on devices that support it and for certain operations, you can store a private key in the Secure Enclave.

在Apple《Certificate, Key, and Trust Services Programming Guide》一文中提到:将私钥保存在钥匙串(keychain)中,仍然有遭到破坏的风险。Apple建议把私钥存储在Secure Enclave中,并且只能存储256位的椭圆曲线私钥。

/** 生成密钥 */
+ (BOOL)generatePrivateKey:(NSString *)attrLabel error:(NSError **)error {
    if (!attrLabel) {
        if (error) {
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionInvalidParameter userInfo:@{NSLocalizedDescriptionKey:@"The 'attrLabel' parameter passed to a function were not valid."}];
        }
        return NO;
    }

    CFErrorRef e = NULL;
    SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                                    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
                                                                    kSecAccessControlPrivateKeyUsage,
                                                                    &e);
    if (e) {
        if (sacObject) CFRelease(sacObject);

        if (error) {
            *error = (__bridge NSError *)e;
        }
        return NO;
    }

    NSDictionary *parameters = @{
                                 (id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
                                 (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
                                 (id)kSecAttrKeySizeInBits: @256,                                 (id)kSecAttrLabel: attrLabel,
                                 (id)kSecPrivateKeyAttrs: @{
                                         (id)kSecAttrAccessControl: (__bridge_transfer id)sacObject,
                                         (id)kSecAttrIsPermanent: @YES,
                                         }
                                 };

    CFErrorRef errorRef = NULL;
    SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)parameters, &errorRef);

    if (errorRef) {
        if (privateKey) CFRelease(privateKey);
        if (error) {
            *error = (__bridge NSError *)errorRef;
        }
        return NO;
    } else if (privateKey) {
        CFRelease(privateKey);

        return YES;
    } else {
        if (privateKey) CFRelease(privateKey);

        return NO;
    }
}

上面的函数是用来生成密钥的。

  • 属性kSecAttrTokenID的值kSecAttrTokenIDSecureEnclave表示将生成的密钥(这里说的是私钥)自动保存在Secure Enclave中。
  • 属性kSecAttrKeyType的值kSecAttrKeyTypeECSECPrimeRandom表示生成那种类型的密钥,这里指ECC(椭圆曲线键)密钥。
  • 属性kSecAttrKeySizeInBits的值表示只存储256位的私钥。
  • 属性kSecAttrIsPermanent表示私钥是否持久化,这里我们设置为YES,表示默认将其存储在钥匙串中。

二、 从钥匙串中获取私钥

+ (SecKeyRef)retrievePrivateFromKeychain:(NSString *)attrLabel error:(NSError **)error CF_RETURNS_RETAINED {
    if (!attrLabel) {
        if (error) {
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionInvalidParameter userInfo:@{NSLocalizedDescriptionKey:@"The 'attrLabel' parameter passed to a function were not valid."}];
        }
        return NULL;
    }

    // Query private key object from the keychain.
    NSDictionary *params = @{
                             (id)kSecClass: (id)kSecClassKey,
                             (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
                             (id)kSecAttrKeySizeInBits: @256,
                             (id)kSecAttrLabel: attrLabel,
                             (id)kSecReturnRef: @YES,
                             };

    // Retrieve the key from the keychain.  No authentication is needed at this point.

    SecKeyRef privateKey = NULL;

    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)params, (CFTypeRef *)&privateKey);

    if (status == errSecSuccess) {
        return privateKey;
    } else {
        if (error) {
            *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:@{NSLocalizedDescriptionKey:ALNSStringFromOSStatusDesc(status)}];
        }
        return NULL;
    }
}

这样就可以从钥匙串中检索出私钥,使用密钥来数字签名或者使用密钥进行加密。

Later, you retrieve the key from the keychain in the usual way, as described in Storing Keys in the Keychain. You also use the key to sign a block of code exactly as you would normally, as described in Signing and Verifying. Alternatively, you can use the key for encryption as described in Using Keys for Encryption and in particular for symmetric encryption using the eciesEncryptionCofactorX963SHA256AESGCM algorithm. Note that in these cases, you are restricted to the elliptic curve algorithms because the Secure Enclave holds only elliptic curve keys.

三、删除私钥

/** 从钥匙串中删除私钥 */
+ (BOOL)deletePrivateKeyFromKeychain:(NSString *)attrLabel error:(NSError **)error {
    if (!attrLabel) {
        if (error) {
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionInvalidParameter userInfo:@{NSLocalizedDescriptionKey:@"The 'attrLabel' parameter passed to a function were not valid."}];
        }
        return NO;
    }
    
    NSDictionary *query = @{
                            (id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
                            (id)kSecClass: (id)kSecClassKey,
                            (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
                            (id)kSecAttrLabel: attrLabel,
                            (id)kSecReturnRef: @YES,
                            };

    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);

    if (status == errSecSuccess) {
        return YES;
    } else {
        if (error) {
            *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:@{NSLocalizedDescriptionKey:ALNSStringFromOSStatusDesc(status)}];
        }
        return NO;
    }
}

从钥匙串中检索到私钥后,通过迪菲-赫尔曼密钥交换协议,创建一个密钥,该密钥可以作为对称密钥来加密数据信息。

迪菲-赫尔曼密钥交换(Diffie–Hellman key exchange,简称“D–H”) 是一种安全协议。它可以让双方在完全没有对方任何预先信息的条件下通过不安全信道建立起一个密钥。这个密钥可以在后续的通讯中作为对称密钥来加密通讯内容。(摘自《百度百科》)

+ (NSString *)generateExchangeKeyWithAttrLabel:(NSString *)aKeyAttrLabel otherAttrLabel:(NSString *)bKeyAttrLabel sharedData:(NSData *)sharedData error:(NSError **)error {

    if (!aKeyAttrLabel || !bKeyAttrLabel || !sharedData) {
        if (error) {
            NSString *descriptionKey = [NSString stringWithFormat:@"%s : One or more parameters passed to a function were not valid.", __func__];
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionInvalidParameter userInfo:@{NSLocalizedDescriptionKey:descriptionKey}];
        }
        return nil;
    }

    SecKeyRef aPrivateKey = [self retrievePrivateFromKeychain:aKeyAttrLabel error:error];
    SecKeyRef bPrivateKey = [self retrievePrivateFromKeychain:bKeyAttrLabel error:error];

    if (aPrivateKey == NULL || bPrivateKey == NULL) {
        if (aPrivateKey) CFRelease(aPrivateKey);
        if (bPrivateKey) CFRelease(bPrivateKey);

        return nil;
    }

    SecKeyRef aPublicKey = SecKeyCopyPublicKey(aPrivateKey);

    CFErrorRef errorRef = NULL;

    NSDictionary *params = @{(id)kSecKeyKeyExchangeParameterRequestedSize : @(sharedData.length),
                              (id)kSecKeyKeyExchangeParameterSharedInfo : sharedData};

    CFDataRef dataRef = SecKeyCopyKeyExchangeResult(bPrivateKey,
                                                    kSecKeyAlgorithmECDHKeyExchangeCofactorX963SHA256,
                                                    aPublicKey,
                                                    (__bridge CFDictionaryRef)params,
                                                    &errorRef);
    if (errorRef) {
        if (dataRef) CFRelease(dataRef);
        if (error) {
            *error = (__bridge NSError *)errorRef;
        }
    } else if (dataRef) {
        NSData *nsData = (__bridge NSData *)dataRef;
        NSString *dataString = [[NSString alloc] initWithData:nsData encoding:NSUTF8StringEncoding];
        if (dataString == nil) { 
	        // 解决NSData转化为NSString时,可能返回nil的情况
            NSData *data = ALUTF8NSData(nsData);
            dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        }

        CFRelease(dataRef);

        if (dataString) {
            if (aPrivateKey) CFRelease(aPrivateKey);
            if (aPublicKey) CFRelease(aPublicKey);
            if (bPrivateKey) CFRelease(bPrivateKey);

            return dataString;
        } else if (error) {
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionNSDataConvertToNSStringFailure userInfo:@{NSLocalizedDescriptionKey:@"The NSData convert to NSString failure."}];
        }
    }

    if(aPublicKey) CFRelease(aPublicKey);
    if(aPrivateKey) CFRelease(aPrivateKey);
    if(bPrivateKey) CFRelease(bPrivateKey);

    return nil;
}

三、 使用密钥对数据进行加解密

static NSString *const KEY_A = @"key_A";
static NSString *const KEY_B = @"key_B";

+ (NSString *)encryption:(NSString *)plainText error:(NSError **)error {
    if (!plainText) {
        if (error) {
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionInvalidParameter userInfo:@{NSLocalizedDescriptionKey:@"The `encrypted` string passed to a function were not valid."}];
        }
        return nil;
    }

    BOOL generatePrivateKeyA = [self generatePrivateKey:KEY_A error:error];
    BOOL gengratePrivateKeyB = [self generatePrivateKey:KEY_B error:error];

    if (generatePrivateKeyA && gengratePrivateKeyB) {
        NSString *identifier = [NSBundle mainBundle].bundleIdentifier;
        NSData *sharedData = [identifier dataUsingEncoding:NSUTF8StringEncoding];
        NSString *key = [self generateExchangeKeyWithAttrLabel:KEY_A otherAttrLabel:KEY_B sharedData:sharedData error:error];
        if (key) {
            NSString *iv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];

            NSData *plainData = [plainText dataUsingEncoding:NSUTF8StringEncoding];

            NSData *encryptedData = [plainData aes256EncryptWithIv:iv forKey:key];

            if (encryptedData) {
                return [encryptedData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
            } else if (error) {
                *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionFailure userInfo:@{NSLocalizedDescriptionKey:@"The AES256 encrypt failure"}];
                return nil;
            }
        }
    }

    return nil;
}

+ (NSString *)decryption:(NSString *)plainText error:(NSError **)error {
    if (!plainText) {
        if (error) {
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataEncryptionInvalidParameter userInfo:@{NSLocalizedDescriptionKey:@"The `decrypted` string passed to a function were not valid."}];
        }
        return nil;
    }

    NSString *identifier = [NSBundle mainBundle].bundleIdentifier;
    NSData *sharedData = [identifier dataUsingEncoding:NSUTF8StringEncoding];
    NSString *key = [self generateExchangeKeyWithAttrLabel:KEY_A otherAttrLabel:KEY_B sharedData:sharedData error:error];

    if (key) {
        NSString *iv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
        NSData *plainData = [[NSData alloc] initWithBase64EncodedString:plainText options:NSDataBase64DecodingIgnoreUnknownCharacters];
        NSData *decryptedData = [plainData aes256DecryptWithIv:iv forKey:key];
        if (decryptedData) {
            NSString *result = [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding];
            return result;
        } else if (error) {
            *error = [NSError errorWithDomain:ALDataEncryptionErrorDomain code:ALDataDecryptionFailure userInfo:@{NSLocalizedDescriptionKey:@"The AES256 decrypt failure"}];
            return nil;
        }
    }

    return nil;
}

以上是使用AES256对称加密算法对字符串进行加解密。

最后,再加解密完成后,从钥匙串移除生成的私钥。

+ (BOOL)cleanPrivateKeyFromKeychainWithError:(NSError * __autoreleasing *)error {
    NSError *err = nil;
    BOOL deletePrivateKeyA = [self deletePrivateKeyFromKeychain:KEY_A error:&err];
    if (err) {
        if (error) *error = err;
        return deletePrivateKeyA;
    }
    BOOL deletePrivateKeyB = [self deletePrivateKeyFromKeychain:KEY_B error:&err];
    if (err) {
        if (error) *error = err;
        return deletePrivateKeyB;
    }
    
    return deletePrivateKeyA && deletePrivateKeyB;
}

注意事项

  • 文中所提到的一些技术是iOS 10.0才有的新特性,使用时请做好版本控制。

  • 经过我们测试发现,在生成私钥的时候,将函数 SecAccessControlRef SecAccessControlCreateWithFlags()的参数protection设置为kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,用CFDataRef _Nullable SecKeyCopyKeyExchangeResult()函数都可以获取到交换后的Data。但是当用户手动删除passcode,再重新添加上passcode后,就一直获取不到交换后的密钥,返回为nil。这必然就会出现漏洞。

  • 关于上述的问题,目前也不清楚是什么原因造成的,所以后面我们替换成了kSecAttrAccessibleWhenUnlockedThisDeviceOnly。幸运的是,上述的问题解决了。即使删除后,重新添加上passcode也会正常获取到交换后的密钥。

  • 可是后来有发现了问题,我们换成kSecAttrAccessibleWhenUnlockedThisDeviceOnly这个key后,在iOS 10.0.1和iOS 10.0.2系统版本的device上还是获取不到交换后的Data,即函数SecKeyCopyKeyExchangeResult()一直返回nil,并返回一个错误Error Domain=com.apple.LocalAuthentication Code=-1009 "ACL operation is not allowed: 'ock'" UserInfo={NSLocalizedDescription=ACL operation is not allowed: 'ock'}。而在iOS 10.1.1,10.2.1, 10.3.1,10.3.2上都是可以正常获取到的。坑爹啊,我能怎么办?我也很无助。

  • 所以在使用该教程中的示例时,要特别小心上述的几个坑。切记,切记。

参考链接

  • https://developer.apple.com/library/content/documentation/Security/Conceptual/CertKeyTrustProgGuide/SecureKeyGen.html
  • https://developer.apple.com/reference/uikit/uidevice/1620059-identifierforvendor?language=objc
  • http://www.cocoachina.com/ios/20151231/14835.html
  • http://blog.csdn.net/lee244868149/article/details/51790397
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!