0x00 前言
这篇文章是自己在学习 CFHipsterRef Chapter 10 Security 时做的笔记。
0x01 Security
使用 Security 框架,应用程序可以以编程方式访问 keychain,允许对用户数据进行受保护访问,不会不断地提示进行身份验证。
不幸的是,最终用户的便利性是以开发者为代价的,因为在 Cocoa 中有一些用来与 Keychain 交互的 API 很麻烦。
与 Keychain 的交互是通过查询来调解的,而不是直接操作。查询本身可能相当复杂,并且使用 C API 很麻烦。
查询是由以下组件组成的字典(dictionary):
- 要搜索的项目的类别,”Generic Password”,”Internet Password”,”Certificate”,”Key” 或 “Identity”。
- 查询返回类型,”Data”,”Attributes”,”Reference” 或 “Persistent Reference”。
- 匹配一个或多个属性(attribute)键值对。
- 一个或多个搜索键值对以进一步修改结果,例如匹配字符串是否区分大小写,只匹配信任的证书,或者限制只有一个结果或返回全部。
字符串常量用于几乎所有的键和很多的值,这使得很多 __bridge id
转换 和文档查找。
0x02 获取 Keychain Items
要获取 Keychain item,先构造一个查询,并传递给 SecItemCopyMatching
:
1 2 3 4 5 6 7 8 9 10 11 12
| NSString *service = @"com.example.app"; NSString *account = @"username";
NSDictionary *query = @{ (__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService : service, (__bridge id)kSecAttrAccount : key, (__bridge id)kSecMatchLimit : kSecMatchLimitOne, };
CFTypeRef result; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
在这个示例中,查询指示 keychain 使用匹配的 username 查找服务 com.example.app
的所有通用密码项。kSecAttrService
定义了凭证的范围,而 kSecAttrAccount
作为一个唯一标识符。传递的搜索选项 kSecMatchLimitOne
确保只返回第一个匹配的结果(如果有的话)。
如果 status
等于 errSecSuccess
(0),则 result
应该被匹配的凭证所填充。
0x03 添加和更新 Keychain Items
也许是 Keychain Services APIs 的主要症结,但是,为了写到 keychain,必须先读取它。有两个写方法:SecItemAdd
和 SecItemUpdate
。用具有已匹配现有 item 的属性调用的 SecItemAdd
会返回状态码 errSecDuplicateItem
。用没有匹配现有 item 的属性调用 SecItemUpdate
会返回状态码 errSecItemNotFound
。因为缺少 UPSERT
类型命令,其中一个是每次都会被 resigned 以有条件地相应:
1 2 3 4 5 6 7 8 9 10 11 12
| NSData *data = ...; if (status == errSecSuccess) { NSDictionary *updatedAttributes = @{(__bridge id)kSecValueData : data}; SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)updatedAttributes); } else { NSMutableDictionary *attributes = [query mutableCopy]; attributes[(__bridge id)kSecValueData] = data; attributes[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlock; SecItemAdd((__bridge CFDictionaryRef)attributes, NULL); }
|
从上一示例开始,使用 kSecValueData
属性 key 在 item 上设置任意数据。原始查询被复制并合并到 SecItemAdd
的另外的属性,而在更新的属性被传给 SecItemUpdate
。
0x04 加密消息语法
加密消息语法(Cryptographic Message Syntax)是 IETF 用于 S/MIME 消息的公钥加密和数字签名的标准。Apple 的加密消息语法服务在 Security 框架中提供了实现这些行业标准算法的 API。
如 Core Services 章节所述,MIME 是使 email,恩,有用的互联网标准。如果没有它,email 将不支持非 ASCII 字符或附件。S/MIME 中的 S 指的是如何安全地发送和接收这些消息。
消息可以由任意数量的签名者或收件人签名或加密,或同时签名和加密。对一个消息签名是允许收件人验证其发件人。对一个消息加密是为了确保这个消息对除了收件人可以解密消息内容之外的任何人都保密。这两个操作是正交的(orthogonal),但是与密码相关。
编码一个消息
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
| NSData *data; SecCertificateRef certificateRef;
CMSEncoderRef encoder; CMSEncoderCreate(&encoder);
CMSEncoderUpdateContent(encoder, [data bytes], [data length]); CMSEncoderAddRecipients(encoder, certificateRef);
SecIdentityRef identityRef = nil; SecIdentityCreateWithCertificate(nil, certificateRef, &identityRef); CMSEncoderUpdateContent(encoder, [data bytes], [data length]); CMSEncoderAddSigners(encoder, identityRef); CFRelease(identityRef);
CMSEncoderUpdateContent(encoder, [data bytes], [data length]); CMSEncoderAddSignedAttributes(encoder, kCMSAttrSmimeCapabilities);
CFDataRef encryptedDataRef; CMSEncoderCopyEncodedContent(encoder, &encryptedDataRef); NSData *encryptedData = [NSData dataWithData:(__bridge NSData *)encryptedDataRef];
CFRelease(encoder);
|
解码一个消息
1 2 3 4 5 6 7 8 9 10 11 12
| CMSDecoderRef decoder; CMSDecoderCreate(&decoder);
CMSDecoderUpdateMessage(decoder, [encryptedData bytes], [encryptedData length]); CMSDecoderFinalizeMessage(decoder);
CFDataRef decryptedDataRef; CMSDecoderCopyContent(decoder, &decryptedDataRef); NSData *decryptedData = [NSData dataWithData:(__bridge NSData *)decryptedDataRef];
CFRelease(decryptedDataRef); CFRelease(decoder);
|
0x05 Certificate, Key 和 Trust Services
数字证书用来验证其持有人和发送人的身份。
理解证书(certificates) 最好的方法是打开一个,看看里面有什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| $ openssl x509 -in certificate.pem -noout -text
Certificate:
Data: Version: 1 (0x0) Serial Number: 4919 (0x1337) Signature Algorithm: md5WithRSAEncryption Issuer: C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com Validity Not Before: Jun 2 18:00:00 2014 GMT Not After : Jun 2 18:00:00 2015 GMT Subject: C=US, ST=Oregon, L=Portland, O=Mattt Thompson, OU=NSHipster, CN=nshipster.com/emailAddress=mattt@nshipster.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public Key: (1024 bit) Modulus (1024 bit): cb:1c:00:aa:bb:89:a0:4c:26:cd:8c:4b:0b:13:88:... Exponent: 65537 (0x10001) Signature Algorithm: md5WithRSAEncryption f5:5c:d6:a0:bf:39:95:fb:fa:ba:f5:f5:5a:d5:d9:f8:42:6b:...
|
与其笨重的声誉相反,证书非常容易解析和理解 —— 总之即使对于没有熟练掌握密码学的人来说。
通过扫描纯文本输出,几个信息浮出表面:
- 证书发行人(Certificate issuer)
- 有效期(Validity period)
- 证书持有者(Certificate holder)
- 所有者的公钥(Public key of the owner)
- 来自证书认证机构的数字签名(Digital signature from the certification authority)
每个证书都由其颁发证书进行验证,如此沿着证书链建立信任,一直到由认证机构颁发的根证书。
证书是用于保护互联网的密码基础结构的基础。具有证书的 iOS 和 OS X 开发者最常见的交互之一是验证来自一个 URL 请求的质询(challenge):
1 2 3 4 5 6 7
| NSURLAuthenticationChallenge *challenge = ...; SecTrustRef trust = challenge.protectionSpace.serverTrust; SecPolicyRef X509Policy = SecPolicyCreateBasicX509(); SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)@[(__bridge id)X509Policy]);
SecTrustResultType result; assert(SecTrustEvaluate(trust, &result) == errSecSuccess);
|
SecTrustEvaluate
通过验证证书的签名以及证书链中的证书签名一起使证书生效(validates),直到锚证书(anchor certificate)。为每个指定的策略创建一个证书链,从 leaf 证书开始,检查链中的每一个证书,直到遇到无效的证书,没有更多的证书,或者发现了非默认信任设置的证书。
在身份认证质询期间验证证书持有者的身份至关重要,因为它可以确保服务器是其声明的,并且可以用敏感信息进行信任。
密码学的根本考虑是保护消息从被发送者加密到被接收者解密的时间的含义。当该消息是原始二进制数据时,有时需要采取额外的步骤将该数据编码成文本表示。
Base64 编码将二进制数据映射到 8 位 chunks,然后由 64 个可打印的 ASCII 字符表示。它对数据进行编码只需要 33% 的开销,不依赖于密文字符,并且具有相对直接的实现。它是一种 UTF-8 的二进制到文本编码。
Base64 用于任何从 HTTP 基本授权到 CSS 文档中嵌入的 data-uri
资源,一直观地比较固定大小的字节序列,例如 MD5 或 SHA-1 校验和。
Security 框架使用 SecTransformExecute
函数为 Base64(以及 Base32)编解码提供了内置的支持。
要对数据进行编码,使用 kSecBase64Encoding
选项创建一个 SecTransformRef
的实例,然后调用 SecTransformExecute
:
Base64 编码
1 2 3 4 5 6 7
| SecTransformRef transform = SecEncodeTransformCreate(kSecBase64Encoding, NULL);
SecTransformSetAttribute(transform, kSecTransformInputAttributeName, (__bridge CFDataRef)data, NULL);
NSData *encodedData = (__bridge_transfer NSData *)SecTransformExecute(transform, NULL);
CFRelease(transform);
|
逆向几乎是相同的,除了传 kSecBase64Decoding
到 SecEncodeTransformCreate
:
Base64 解码
1 2 3 4
| SecTransformRef transform = SecEncodeTransformCreate(kSecBase64Decoding, NULL); NSData *decodedData = (__bridge_transfer NSData *)SecTransformExecute(transform, NULL);
CFRelease(transform);
|
0x07 Randomization Services
密码学是基于不可预测的随机值。没有什么保证,它只是安全剧场(security theater)。
SecRandomCopyBytes
从 /dev/random
读取,生成密码安全的随机字节。/dev/random
是在 Unix 上基于设备的环境噪声流熵(streams entropy)的一个特殊的文件。
1 2 3 4 5 6 7
| NSUInteger length = 1024;
NSMutableData *mutableData = [NSMutableData dataWithLength:length];
OSStatus success = SecRandomCopyBytes(kSecRandomDefault, length, mutableData.mutableBytes);
__Require_noErr(success, exit);
|
0x08 CommonCrypto
CommonCrypto 提供了方便的常见加密操作的 API,可以在 iOS 5.0+ 和 OS X 10.5+ 上使用。
0x0801 摘要(Digests)
加密 hash 函数在信息安全中扮演了重要的角色。被称为校验和(checksums),指纹或摘要,加密 hash 函数的输出几乎不能被逆向以找到输入。
例如,”NSHipster” 的 SHA-1 校验和为 7c33b28cb6fe3515548ee58812131de07afeef1b
,而对 “CFHipsterRef” 执行相同的 hash 函数生成 “342924012ebde06234135698b372e10c5b86c5b2”。
要在代码中计算校验和,请使用 CC_SHA1
函数:
1 2 3 4 5 6
| NSData *data = ...;
uint8_t output[CC_SHA1_DIGEST_LENGTH]; cc_SHA1(data.bytes, data.length, output);
NSData *digest = [NSData dataWithBytes:output length:CC_SHA1_DIGEST_LENGTH];
|
有很多加密 hash 函数,每个都有不同的安全特性和用例。开发者有责任评估自己产品的要求,以确定最合适的安全技术。
0x09 HMAC
密钥散列消息认证码(keyed-hash message authentication code, HMAC)使用加密 hash 函数和密钥来生成可用于同时验证消息的完整性和真实性的代码。HMAC 的强度取决于加密 hash 函数的强度以及密钥的大小。HMAC 通常由 web 服务使用以确保受保护的调用只有被验证的用户可以访问。
Common Crypto 提供了 CCHmac
用于生成 HMAC:
1 2 3 4 5 6
| NSData *data, *key;
unsigned int length = CC_SHA1_DIGEST_LENGTH; unsigned char output[length];
CCHmac(kCCHmacAlgSHA1, key.bytes, key.length, data.bytes, data.length, output);
|
0x0A Symmetric Encryption
在作者撰写此文时,AES-128 & PBKDF2 是安全对称加密的一种合理方法,即加密和解密消息。
高级加密标准(The Advanced Encryption Standard, AES)是由美国国家标准和技术研究所(NIST)建立的一种加密规范。PBKDF2 是一种使用 hash 函数的方法,通常用于生成一个 block 或 序列码(stream cypher)的 key,就像 AES。
Security 框架提供了构建 block 来进行对称加密,但是需要开发者自己实现特定的实现。
第一步是创建一个函数,该函数从一个加密的密码生成一个 PBKDF2 key。Salt 是用作对密码执行单向函数的附加输入的随机数据。
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
| static NSData *AES128PBKDF2KeyWithPassword(NSString *password, NSData *salt, NSError * __autoreleasing *error) { NSCParameterAssert(password); NSCParameterAssert(salt); NSMutableData *mutableDerivedKey = [NSMutableData dataWithLength:kCCKeySizeAES128]; CCCryptorStatus status = CCKeyDerivationPBKDF(kCCPBKDF2, [password UTF8String], [password lengthOfBytesUsingEncoding:NSUTF8StringEncoding], [salt bytes], [salt length], kCCPRFHmacAlgSHA256, 1024, [mutableDerivedKey mutableBytes], kCCKeySizeAES128); NSData *derivedKey = nil; if (status != kCCSuccess) { if (error) { *error = [[NSError alloc] initWithDomain:nil code:status userInfo:nil]; } } else { derivedKey = [NSData dataWithData:mutableDerivedKey]; } return derivedKey; }
|
接下来,可以创建加密数据的函数,其将数据和密码加密,并将生成的 salt 和 初始值返回,以及执行操作的时候遇到的错误返回为输出参数:
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
| static NSData *AES128EncryotedDataWithData(NSData *data, NSString *password, NSData * __autoreleasing *salt, NSData * __autoreleasing *initializationVector, NSError * __autoreleasing *error) { NSCParameterAssert(initializationVector); NSCParameterAssert(salt); uint8_t *saltBuffer = malloc(8); SecRandomCopyBytes(kSecRandomDefault, 8, saltBuffer); *salt = [NSData dataWithBytes:saltBuffer length:8]; NSData *key = AES128PBKDF2KeyWithPassword(password, *salt, error); uint8_t *initializationVectorBuffer = malloc(kCCBlockSizeAES128); SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, initializationVectorBuffer); *initializationVector = [NSData dataWithBytes:initializationVector length:kCCBlockSizeAES128]; size_t size = [data length] + kCCBlockSizeAES128; void *buffer = malloc(size); size_t numberOfBytesEncrypted = 0; CCCryptorStatus status = CCCrypt(kCCEncrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding, [key bytes], [key length], [*initializationVector bytes], [data bytes], [data length], buffer, size, &numberOfBytesEncrypted); NSData *encryptedData = nil; if (status != kCCSuccess) { if (error) { *error = [[NSError alloc] initWithDomain:nil code:status userInfo:nil]; } } else { encryptedData = [[NSData alloc] initWithBytes:buffer length:numberOfBytesEncrypted]; } return encryptedData; }
|
最后,为了加密数据,逆向执行相同的过程,这次传递数据和密码以及从加密函数生成的 salt 和初始化向量:
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
| static NSData *AES128DecryptedDataWithData(NSData *data, NSString *password, NSData *salt, NSData *initializationVector, NSError * __autoreleasing *error) { NSData *key = AES128PBKDF2KeyWithPassword(password, salt, error); size_t size = [data length] + kCCBlockSizeAES128; void *buffer = malloc(size); size_t numberOfBytesDecrypted = 0; CCCryptorStatus status = CCCrypt(kCCCDecrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding, [key bytes], [key length], [initializationVector bytes], [data bytes], [data length], buffer, size, &numberOfBytesDecrypted); NSData *encryptedData = nil; if (status != kCCSuccess) { if (error) { *error = [[NSError alloc] initWithDomain:nil code:status userInfo:nil]; } } else { encryptedData = [[NSData alloc] initWithBytes:buffer length:numberOfBytesDecrypted]; } return encryptedData; }
|