本篇文章会重在如何调用api
,原理不会深入,网上教程比较多。
下面是我的封装:
Base64
个人觉得,Base64
并不是一种加密方式,而是一种编码方式,和ASCII
码一样。
ASCII
码用数值0-127
表示128
个字符,也就是2^7
,ASCII
码的扩展能表达出2^8
个字符,而Base64
则用0-63
表示2^6
个字符,具体表如下:
我们可以发现这些字符都是比较好表达的,在ASCII
表中有些符号不好展示出来,所以Base64
更容易被我们复制黏贴。
除了表格中的字符,我们常常看到Base64
字符串后面常常接着=
,=
表示的也是0
,但是这个比较特殊,因为Base64
编码的时候是以6个bit位编码的,而我们传输单位的字节是8bit
的,Base64
编码时常会发生不能整除的数据的情况,所以要最后补上0
,而这些0
会被编码成=
,在解码的时候,=
的0
会被忽略。
我们看下在iOS中的调用:
- OC代码:
// Data转成Base64编码字符串
NSString *string = [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
// Base64编码转成Data
[[NSData alloc] initWithBase64EncodedString:base64Str options:NSDataBase64DecodingIgnoreUnknownCharacters];
复制代码
- Swift代码:
// Data转成Base64编码字符串
data.base64EncodedString()
// Base64编码转成Data
let data = Data.init(base64Encoded: base64Str)
复制代码
散列函数
散列函数又称哈希函数,具体原理可以看我前面写的探索Swift中Dictionary的底层实现及原理
苹果加密这块用的是CommonCrypto
,所以在用加密前都要导入这个框架(iOS13推出了新的框架CryptoKit
)
我们看下苹果支持多少种哈希函数
enum {
kCCHmacAlgSHA1,
kCCHmacAlgMD5,
kCCHmacAlgSHA256,
kCCHmacAlgSHA384,
kCCHmacAlgSHA512,
kCCHmacAlgSHA224
};
typedef uint32_t CCHmacAlgorithm;
复制代码
从Hmac
的枚举值种可以看到,支持SHA1
、MD5
、SHA256
、SHA384
、SHA512
、SHA224
,其他枚举值写的太零散了,不过CommonCrypto
支持这6种。
不同哈希函数结果的字节长度也不一样:
#define CC_MD5_DIGEST_LENGTH 16 /* digest length in bytes */
#define CC_SHA1_DIGEST_LENGTH 20 /* digest length in bytes */
#define CC_SHA224_DIGEST_LENGTH 28 /* digest length in bytes */
#define CC_SHA256_DIGEST_LENGTH 32 /* digest length in bytes */
#define CC_SHA384_DIGEST_LENGTH 48 /* digest length in bytes */
#define CC_SHA512_DIGEST_LENGTH 64 /* digest length in bytes */
复制代码
我们看下swift实现:
func hashData(data: Data) -> Data {
var digest = Data(count: Int(hashDigestLength))
digest.withUnsafeMutableBytes { (digestPtr: UnsafeMutablePointer<UInt8>) in
// 这里碰到一个坑,Data的长度不一样,内存结构也会不一样(测下来15),本来data.withUnsafeBytes { $0 }只能用NSData(data: data).bytes了
let dataPtr = NSData(data: data).bytes
switch type {
case .MD5:
CC_MD5(dataPtr, numericCast(data.count), digestPtr)
case .SHA1:
CC_SHA1(dataPtr, numericCast(data.count), digestPtr)
case .SHA224:
CC_SHA224(dataPtr, numericCast(data.count), digestPtr)
case .SHA256:
CC_SHA256(dataPtr, numericCast(data.count), digestPtr)
case .SHA384:
CC_SHA384(dataPtr, numericCast(data.count), digestPtr)
case .SHA512:
CC_SHA512(dataPtr, numericCast(data.count), digestPtr)
}
}
return digest
}
复制代码
OC版本可以在我封装的代码里面看。
加盐
虽然说哈希函数是一个不可逆的过程,理论上从结果上无法推断出原文是什么,但是网上还是有人才提供了一个数据库,里面存放了常见的字符串与对应的hash值,例如初始密码123456
,就能从hash值查表的方式找到原文
那怎么办呢,我们可以在我们的字符串的后面加一串自定义的字符串,这样可以添加被破解的复杂度,这样的行为我们称之为加盐。但是这样比较low,所以系统提供了Hmac
方式的哈希函数:
func hmacHashData(data: Data, hmacKey: Data) -> Data {
var digest = Data(count: Int(hashDigestLength))
digest.withUnsafeMutableBytes {
CCHmac(CCHmacAlgorithm(hmacAlgorithm), NSData(data: hmacKey).bytes, hmacKey.count, NSData(data: data).bytes, data.count, $0)
}
return digest
}
复制代码
计算大文件的hash值
计算文件的hash值,我们可以先把文件读到内存中变成数据,然后调用api得到hash值,但是有一个问题,如果文件非常大的话,那么会占用非常大的内存,说不定导致APP内存溢出了,那怎么办呢?
框架给我们提供分布读取data进行hash的api:
CC_MD5_CTX hashCtx;
CC_MD5_Init(&hashCtx);
while (YES) {
@autoreleasepool {
NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData];
CC_MD5_Update(&hashCtx, data.bytes, (CC_LONG)data.length);
if (data.length == 0) {
break;
}
}
}
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
CC_MD5_Final(buffer, &hashCtx);
result = [NSData dataWithBytes:buffer length:CC_MD5_DIGEST_LENGTH];
复制代码
在循环内部放一个自动释放池@autoreleasepool
,这样就能及时释放内存了。
对称加密
对称加密在CommonCrypto
就一个核心的api,把这个api搞懂了,那么就能把对称加密掌握了,我们看下这个api:
CCCryptorStatus CCCrypt(
CCOperation op, /* kCCEncrypt, etc. */
CCAlgorithm alg, /* kCCAlgorithmAES128, etc. */
CCOptions options, /* kCCOptionPKCS7Padding, etc. */
const void *key,
size_t keyLength,
const void *iv, /* optional initialization vector */
const void *dataIn, /* optional per op and alg */
size_t dataInLength,
void *dataOut, /* data RETURNED here */
size_t dataOutAvailable,
size_t *dataOutMoved)
API_AVAILABLE(macos(10.4), ios(2.0));
复制代码
我们分析下各个参数
CCOperation
这是一个枚举值,很简单:
enum {
kCCEncrypt = 0,
kCCDecrypt,
};
typedef uint32_t CCOperation;
复制代码
一个是加密一个是解密
CCAlgorithm
这个是CommonCrypto
支持对称加密种类的枚举值
enum {
kCCAlgorithmAES128 = 0, /* Deprecated, name phased out due to ambiguity with key size */
kCCAlgorithmAES = 0,
kCCAlgorithmDES,
kCCAlgorithm3DES,
kCCAlgorithmCAST,
kCCAlgorithmRC4,
kCCAlgorithmRC2,
kCCAlgorithmBlowfish
};
typedef uint32_t CCAlgorithm;
复制代码
第一个kCCAlgorithmAES128
第一个被废弃了,用了也等于kCCAlgorithmAES
CCOptions
这个是对称加密的一些可选项:
enum {
/* options for block ciphers */
kCCOptionPKCS7Padding = 0x0001,
kCCOptionECBMode = 0x0002
/* stream ciphers currently have no options */
};
typedef uint32_t CCOptions;
复制代码
虽然只有两个枚举,但应该是设计成可选枚举的,所以我们用这个枚举值的时候用位运算符。
填充模式
我们先看第一个kCCOptionPKCS7Padding
,这是一个填充模式,不使用枚举值的时候是ZeroPadding
,和NoPadding
一个意思。
说填充模式前,我们先认识下块(Block
)的概念,对称加密算法对数据加密都是一段数据一段数据加密,每一段数据都被称之为块Block
,不同加密算法的块(数据)长度是不一样的,我们看下对应的枚举值:
enum {
/* AES */
kCCBlockSizeAES128 = 16,
/* DES */
kCCBlockSizeDES = 8,
/* 3DES */
kCCBlockSize3DES = 8,
/* CAST */
kCCBlockSizeCAST = 8,
kCCBlockSizeRC2 = 8,
kCCBlockSizeBlowfish = 8,
};
复制代码
说完这个我们回到填充模式
ZeroPadding
:数据长度不对齐(数据长度不能被块Block
大小整除)时使用0填充,否则不填充。PKCS7Padding
:数据长度不对齐时使用数字n填充至对齐,数据长度已经对齐时后面填充一块块Block
长度的n数字数据,n为你所补充的字节长度。PKCS5Padding
:PKCS7Padding的子集,块大小固定为8字节,也就是n固定为8。(iOS中并没有这个选项 = =!)
加密方式
我们看到第二个枚举值是kCCOptionECBMode
,如果不选,默认是ECBMode
。
ECB(Electronic Code Book)
:电子密码本模式。每一块数据,独立加密。最基本的加密模式,也就是通常理解的加密,相同的明文将永远加密成相同的密文,无初始向量,容易受到密码本重放攻击,一般情况下很少用。
这里的每一块指的就是前面说的Block
,虽然上面介绍说很少使用,但是在应用中,因为比较简单,没有向量参数,平时反而用的多。 = =
CBC(Cipher Block Chaining)
:密码分组链接模式。使用一个密钥和一个初始化向量[IV]对数据执行加密。明文被加密前要与前面的密文进行异或运算后再加密,因此只要选择不同的初始向量,相同的密文加密后会形成不同的密文,这是目前应用最广泛的模式。CBC加密后的密文是上下文相关的,但明文的错误不会传递到后续分组,但如果一个分组丢失,后面的分组将全部作废(同步错误)。CBC可以有效的保证密文的完整性,如果一个数据块在传递是丢失或改变,后面的数据将无法正常解密。
与ECB
相比,CBC
更加安全,因为CBC
的每一块数据加密都和前一块数据有关,所以在CBC
模式下,如果你只拿到中间一段加密数据,即使有秘钥,也解不开数据。但我个人觉得,在APP下抓包请求,一般都能拿到完整的加密数据,所以CBC
模式的这个特性,在这个场景下好像并没有多少优势,所以平时用ECB
会多一点。
key
与keyLength
key
需要传入的是秘钥data的指针,这个没什么好说的,最关键的还是keyLength
,每种加密方式需要的秘密长度是不一样的,具体的在框架中用枚举已经体现出来了:
*/
enum {
kCCKeySizeAES128 = 16,
kCCKeySizeAES192 = 24,
kCCKeySizeAES256 = 32,
kCCKeySizeDES = 8,
kCCKeySize3DES = 24,
kCCKeySizeMinCAST = 5,
kCCKeySizeMaxCAST = 16,
kCCKeySizeMinRC4 = 1,
kCCKeySizeMaxRC4 = 512,
kCCKeySizeMinRC2 = 1,
kCCKeySizeMaxRC2 = 128,
kCCKeySizeMinBlowfish = 8,
kCCKeySizeMaxBlowfish = 56,
};
复制代码
其中AES
可以有3种不同的长度,分别对应了16,24,32个字节。
AES
、DES
、3DES
的秘钥长度都是固定的,而CAST
、RC4
、RC2
、Blowfish
的秘钥长度都是浮动的,这里给出了最大值及最小值,确保自己的秘钥长度在这区间之内
iv
关于iv
前面提到过了,这个是独属于CBC
加密模式的,所以如果前面的option
内用的ECB
模式,iv
这个参数毫无作用。iv
参数传的也是数据data指针,但是没有让你传长度,这个是因为iv
的数据长度是和你加密的块Block
长度是一致的,一旦你的加密模式确定了,那么iv
的数据长度也是确定的,具体长度值看:
enum {
/* AES */
kCCBlockSizeAES128 = 16,
/* DES */
kCCBlockSizeDES = 8,
/* 3DES */
kCCBlockSize3DES = 8,
/* CAST */
kCCBlockSizeCAST = 8,
kCCBlockSizeRC2 = 8,
kCCBlockSizeBlowfish = 8,
};
复制代码
如果你传的iv
数据长度和加密类型不一致,那么短的会补0,长的会被截断。
dataIn
与dataInLength
这两个比较简单,传的就是你需要加密或解密数据的数据指针和数据长度
dataOut
、dataOutAvailable
与dataOutMoved
这3个参数是用来获取加密或者解密后的数据的。
我们需要申请一块内存空间传递给函数,函数会在这块空间内做加密处理,并且函数处理完的数据也会放在空间内。
那么dataOut
就是你申请的内存空间的指针,最好堆空间,因为并不能确定加解密数据有多大。
dataOutAvailable
就是你申请内存空间的长度了,那么申请多大的空间合适呢?上面在填充模式的时候也说过了,在PKCS7Padding
模式下,如果字节已经对齐,那么会在最后填充一块块Block
大小的填充数据,而且这种情况也是在这么多填充模式下数据最长的情况,所以我们申请空间的时候就以加密数据data
的length
加上blockSize
就行了:
// setup output buffer
size_t bufferSize = [data length] + blockSize;
void *buffer = malloc(bufferSize);
};
复制代码
dataOutMoved
这个值就是获取结果的数据长度,是一个size_t *
类型,所以你传一个size_t
变量的指针过去,等加密或解密操作结束正确后,变量就会拿到结果data真正的长度
CCCryptorStatus
CCCryptorStatus
是函数方法CCCrypt
的返回值,如果是0,那么说明你调用CCCrypt
成功加密或者解密了,如果不是0,而是其他值,可以参考:
enum {
kCCSuccess = 0,
kCCParamError = -4300,
kCCBufferTooSmall = -4301,
kCCMemoryFailure = -4302,
kCCAlignmentError = -4303,
kCCDecodeError = -4304,
kCCUnimplemented = -4305,
kCCOverflow = -4306,
kCCRNGFailure = -4307,
kCCUnspecifiedError = -4308,
kCCCallSequenceError= -4309,
kCCKeySizeError = -4310,
kCCInvalidKey = -4311,
};
复制代码
有关函数方法CCCrypt
已经讲完了,具体使用看GitHub上的封装
非对称加密RSA
RSA
是常见的非对称加密之一,也是我们日常APP开发遇到最多的非对称加密
关于RSA
的介绍就不多说了,关于概念和原理网上有很多的,这里说下RSA
在iOS
下的细节。
RSA
在iOS
中不导入三方框架的情况下,貌似只能依靠钥匙串的框架Security
实现,具体使用看GitHub。
有很多都是固定用法没什么好讲的,说下几个注意点
- 系统
iOS 10
开始支持pem
文件的内容的解析,所以我封装框架最低系统支持也是iOS 10
,主要是其中的一个方法可以通过秘钥的data来生成ref
对象:
SecKeyRef _Nullable SecKeyCreateWithData(CFDataRef keyData, CFDictionaryRef attributes, CFErrorRef *error)
__OSX_AVAILABLE(10.12) __IOS_AVAILABLE(10.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
复制代码
-
RSA
默认的填充方式是PKCS1
,里面填充的值是随机值,所以每次加密的结果都不一样,如果你换成None
不填充的话,那么不会填充数值,所以每次加密结果都一样。框架里还支持一种OAEP
的填充方式,不是重点就不说了,感兴趣的自己搜下 -
关于数据长度的,
RSA
加密数据的长度是有限制的,具体的可以调用:
size_t blockSize = SecKeyGetBlockSize(keyRef);
复制代码
这个值是你能加密数据长度的最大值,但是由于填充数据的存在,你必须还要减掉一定的长度,PKCS1
需要减掉11,OAEP
需要减掉42。有些人封装的时候,如果超过最大的长度,直接会抛出错误,而我封装的会帮你分段加密后拼在一起,但还是不建议让加密数据过长
- 关于签名:
typedef CF_OPTIONS(uint32_t, SecPadding)
{
kSecPaddingNone = 0,
kSecPaddingPKCS1 = 1,
kSecPaddingOAEP = 2, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0),
/* For SecKeyRawSign/SecKeyRawVerify only,
ECDSA signature is raw byte format {r,s}, big endian.
First half is r, second half is s */
kSecPaddingSigRaw = 0x4000,
/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is an MD2
hash; standard ASN.1 padding will be done, as well as PKCS1 padding
of the underlying RSA operation. */
kSecPaddingPKCS1MD2 = 0x8000, // __OSX_DEPRECATED(10.0, 10.12, "MD2 is deprecated") __IOS_DEPRECATED(2.0, 5.0, "MD2 is deprecated") __TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE,
/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is an MD5
hash; standard ASN.1 padding will be done, as well as PKCS1 padding
of the underlying RSA operation. */
kSecPaddingPKCS1MD5 = 0x8001, // __OSX_DEPRECATED(10.0, 10.12, "MD5 is deprecated") __IOS_DEPRECATED(2.0, 5.0, "MD5 is deprecated") __TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE,
/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA1
hash; standard ASN.1 padding will be done, as well as PKCS1 padding
of the underlying RSA operation. */
kSecPaddingPKCS1SHA1 = 0x8002,
/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA224
hash; standard ASN.1 padding will be done, as well as PKCS1 padding
of the underlying RSA operation. */
kSecPaddingPKCS1SHA224 = 0x8003, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),
/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA256
hash; standard ASN.1 padding will be done, as well as PKCS1 padding
of the underlying RSA operation. */
kSecPaddingPKCS1SHA256 = 0x8004, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),
/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA384
hash; standard ASN.1 padding will be done, as well as PKCS1 padding
of the underlying RSA operation. */
kSecPaddingPKCS1SHA384 = 0x8005, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),
/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA512
hash; standard ASN.1 padding will be done, as well as PKCS1 padding
of the underlying RSA operation. */
kSecPaddingPKCS1SHA512 = 0x8006, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),
};
复制代码
这边枚举值的前3个用于填充模式,后面的用于签名与签名验证。md2
和md5
已经被废弃了,剩下了SHA1
、SHA224
、SHA256
、SHA384
、SHA512
。具体签名看代码。