E7abf11594780ef5bdff5273c8c2f842
核心知识篇:密码学和加解密

1.引言

在实际的项目开发中,我们知道,为了保护用户的安全,对于关键的信息,有两个注意点:

  • 网络请求中,不能明文传输用户的隐私数据;
  • 本地明文保存用户的隐私数据。

那么怎么保护用户的隐私数据呢?这就是本文要讨论的:加密。

2.密码学

密码学(英语:Cryptography)可分为古典密码学和现代密码学。在西方语文中,密码学一词源于希腊语kryptós“隐藏的”,和gráphein“书写”。古典密码学主要关注信息的保密书写和传递,以及与其相对应的破译方法。而现代密码学不只关注信息保密问题,还同时涉及信息完整性验证(消息验证码)、信息发布的不可抵赖性(数字签名)、以及在分布式计算中产生的来源于内部和外部的攻击的所有信息安全问题。古典密码学与现代密码学的重要区别在于,古典密码学的编码和破译通常依赖于设计者和敌手的创造力与技巧,作为一种实用性艺术存在,并没有对于密码学原件的清晰定义。而现代密码学则起源于20世纪末出现的大量相关理论,这些理论使得现代密码学成为了一种可以系统而严格地学习的科学。 -- 维基百科

2.1发展历史

古中国周朝

  • 阴符:是以八等长度的符来表达不同的消息和指令,可算是密码学中的替代法,把信息转变成敌人看不懂的符号。
  • 阴书:则运用了移位法,把书一分为三,分三人传递,要把三份书重新拼合才能获得还原的信息。

凯撒密码

据传由古罗马帝国的皇帝凯撒所发明,用在与远方将领的通信上,每个字母被往后位移三格字母所取代。

二战

雪毕伍斯(Arthur Scherbius)发明了“谜”(ENIGMA,恩尼格玛密码机),用于军事和商业上。“谜”主要由键盘、编码器和灯板组成。三组编码器合、加上接线器和其他配件,合共提供了种一亿亿种编码的可能性。

现代密码学

RSA 是 1977 年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。是一种非对称加密算法,在公开密钥加密和电子商业中被广泛使用。

RSA的加密算法是公开的:

  • 公钥加密,私钥解密;
  • 私钥加密,公钥解密。

2.2 加密算法的分类

  • 哈希(散列)函数:不可逆的,用于密码/文件识别。
    • MD5
    • SHA1
    • SHA256/512
  • 对称加密算法:传统加密算法,可逆。
    • DES:数据加密标准(用的比较少,加密强度不够)
    • 3DES:使用3个秘钥,对相同的数据执行3次加密,强度增强
    • AES:高级密码标准,美国国家安全局使用的,iOS系统使用的加密方式(钥匙串)
  • 非对称加密算法:现代加密算法,可逆
    • RSA

2.3 加密算法的特点

散列函数

  • 算法公开;
  • 对相同的数据加密,得到的结果是一样的;
  • 对不同的数据加密,得到的结果是定长的(MD5对不同的数据进行加密,得到的都是32个字符);
  • 也称数据摘要/数据“指纹”,是用来做数据识别的;
  • 不可逆的。

对称加密算法

  • 加密和解密使用同一个 “秘钥”;
  • 秘钥的保密工作,非常重要,一般会定期更换。

非对称加密算法

  • 使用公钥/私钥;
  • 公钥加密,私钥解密;
  • 私钥加密,公钥解密。

3.哈希/散列函数

哈希/散列函数加密方式,是我们在开发中,经常使用的。特别是在登录密码的使用上,大部分都会用到 MD5 进行加密。因为服务器是不需要知道原始密码的,在注册/修改密码时,服务器存储的是加密后的密码,并保存在数据库中。

小示例

- (IBAction)btnLoginClick:(id)sender {
    NSString *userName = self.tfAccount.text;
    NSString *pwd = self.tfPwd.text;  //明文密码 111111
    pwd = pwd.md5String;
    NSLog(@"%@", pwd);
    if ([self loginWithUserName:userName pwd:pwd]) {
        NSLog(@"Login Success");
    }
    else {
        NSLog(@"Login Fail");
    }
}

- (BOOL)loginWithUserName:(NSString *)userName pwd:(NSString *)pwd {
    // 服务器保存的是加密后的密码
    if ([@"LeeGof" isEqualToString:userName] && [@"96e79218965eb72c92a549dd5a330112" isEqualToString:pwd]) {
        return YES;
    }
    return NO;
}

当然,除了在登录密码这块,可以使用哈希/散列函数的加密方式,还有其他常见的应用:

  • 搜索:对空格分隔的字符分别做 md5,结果取并集;
  • 版权:不同的数据 md5 之后差别特别大。原始文件经过 md5 之后,是原始版本,具有版权。后续的盗版文件,md5 是不一样的,说明不是原版;
  • ...

破解

  • 破解网站:虽然哈希/散列函数的加密方式是不可逆的,但有一个网站,可以通过输入加密字符串,得到原始字符串。这个网站是:https://www.cmd5.com/ 。在这个网站的最上面,可以看到它破解的原理:本站针对md5、sha1等全球通用公开的加密算法进行反向查询,通过穷举字符组合的方式,创建了明文密文对应查询数据库,创建的记录约90万亿条,占用硬盘超过500TB,查询成功率95%以上,很多复杂密文只有本站才可查询。自2006年已稳定运行十余年,国内外享有盛誉。
  • 散列碰撞:找出不同的数据使用 md5 之后能够得到相同的结果,这种比较难,只是存在这种可能性。

安全

上面说了破解方法,既然可以破解,那么有没有办法防护呢?实际上,任何方式的加密,都没有绝对的安全防护方式,只能通过一些手段,来尽可能的提高安全性。

  • MD5 加盐
static NSString *salt = @"dfwerqadofadfkjhgie";

- (IBAction)btnLoginClick:(id)sender {
    NSString *userName = self.tfAccount.text;
    NSString *pwd = self.tfPwd.text;  //明文密码 111111
    pwd = [pwd stringByAppendingString:salt].md5String; //10c0fcd9e14976b432f8cb4cc31612e4
    NSLog(@"%@", pwd);
    if ([self loginWithUserName:userName pwd:pwd]) {
        NSLog(@"Login Success");
    }
    else {
        NSLog(@"Login Fail");
    }
}

将加密后的结果到破解网站搜索,可以看到已经查不到密码了。那么这种加盐的方式是不是绝对安全的?
这种加盐的方式,最大的问题在于,盐在各端都是可以看到的(iOS 端、安卓端、后端、前端...)。这种安全隐患是很大的。一旦盐泄露,所有用户的数据就会存在较大的安全问题,并且盐在正式使用之后,是没法去做替换的。那么有没有解决方案呢?我们可以使用动态盐的方式,每个用户的盐是不同的。

  • HMAC。给定一个秘钥,对明文进行秘钥拼接后,对结果做两次散列,得到32位结果。
- (IBAction)btnLoginClick:(id)sender {
    NSString *userName = self.tfAccount.text;
    NSString *pwd = self.tfPwd.text;  //明文密码 111111
    //在实际开发中,秘钥Key来自服务器(注册的时候返回秘钥Key)。客户端拿到 Key,存储到本地。切换新的设备,需要重新找服务器获取
    pwd = [pwd hmacMD5StringWithKey:@"Gof"];  //914b52a37904d0f464c8854d392faa32
    NSLog(@"%@", pwd);
    if ([self loginWithUserName:userName pwd:pwd]) {
        NSLog(@"Login Success");
    }
    else {
        NSLog(@"Login Fail");
    }
}

HMAC 的问题是,当切换新设备时,需要重新找服务器获取 Key。这时候服务器需要校验,一般是服务器会像你之前的设备发一个信息(比如微信/QQ的登录新设备流程)。验证通过之后,才会下发 Key。

思考:如果黑客直接模拟网络请求,用加密后的信息,获得登录的权限。这种情况,如何避免呢?
可以让加密后的密码,具有时间限制。比如限制为1分钟之内可以调用。可以在客户端用 HMAC 加密后的密码拼接上时间,做一次 MD5,服务端也同样用对应的时间,把 加密后的密码拼接上时间,做 MD5,然后进行比对。
这样可以使得黑客,最多只能在这 1 分钟进行操作。当然这也有个漏洞:客户端的时间有可能不准(比如说有的设备改了时间),这时可以用服务端返回的时间来进行 MD5。

钥匙串的访问

通常,我们登录成功之后,后续不再需要登录了。这个时候,我们会在本地保存一些用户登录的相关信息(比如说需要在退出登录到登录页面时,需要在用户名和密码输入框中显示之前的账号信息)。我们可以使用什么方式来保存账号信息呢?

  • NSUserDefaults
  • 钥匙串访问:iOS 7.0.3 版本提供

这里我们重点看钥匙串访问。
钥匙串是苹果提供的非常安全的一种加密方式,使用的是 AES 加密方法,从 iOS 7.0.3 版本开始提供给开发者使用。

  • 通过钥匙串,可以在 Mac 上动态生成复杂密码,帮助用户记住密码。
  • 如果用户访问网站,选择了记住密码,我们还可以看到记住的密码明文。
  • 采用的是 AES 加密,绝对安全。
/// 保存用户信息
/// @param userName 用户名
/// @param pwd 密码
- (void)savePwdWithUserName:(NSString *)userName pwd:(NSString *)pwd {
    //保存账号
    [[NSUserDefaults standardUserDefaults] setObject:userName forKey:@"GofLoginUserName"];
    //同步保存
    [[NSUserDefaults standardUserDefaults] synchronize];
    //保存密码
    if (userName.length > 0 && pwd.length > 0) {
        /**
         第1个参数:密码明文
         第2个参数:服务(因为钥匙串不只是服务于一个 App),App 的一个标识,建议使用 bundleID
         第3个参数:账号,用户名
         */
        [SSKeychain setPassword:pwd forService:@"com.gof.security" account:userName];
    }
    [SSKeychain allAccounts];
}

/// 加载本地用户信息
- (void)loadUserInfo {
    // 加载账号
    self.tfAccount.text = [[NSUserDefaults standardUserDefaults] objectForKey:@"GofLoginUserName"];
    // 获取所有钥匙串中账号(如果没有读取到,查看一下是否开启了钥匙串权限)
    NSLog(@"%@", [SSKeychain allAccounts]);
    // 加载密码
    self.tfPwd.text = [SSKeychain passwordForService:@"com.gof.security" account:self.tfAccount.text];
}

指纹/面容识别

指纹识别是在 iPhone 5S 开始推出,并且是在 iOS 8.0 之后,才开放了指纹识别的 SDK。

- (void)laContext {
    LAContext *ctx = [[LAContext alloc] init];

    // 判断设备是否支持指纹识别
    if ([ctx canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:NULL]) {
        NSLog(@"Support");
        // 输入指纹/面容
        [ctx evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"指纹/面容支付" reply:^(BOOL success, NSError * _Nullable error) {
            NSLog(@"Success : %d error : %@", success, error);
        }];
    }
    else {
        NSLog(@"Do not support");
    }
}

指纹/面容 和 密码的区别:

  • 指纹/面容:代表的是这个手机的主人;
  • 密码:代表的是这个账号的主人。

4.对称加密算法

4.1两种加密方式

ECB(电子代码本)

  • 加密:将一个大的数据块,拆分成若干个独立的数据块,依次独立加密,最后拼接起来。
  • 解密:将加密的数据,拆分成若干个独立的数据块,依次解密,最后拼接起来。

CBC(密码块链)

  • 加密:将一个大的数据块,拆分成若干个数据块,加密第二块数据的时候,是和第一块数据的加密结果有关系的。也就是下一块数据的加密依赖上一块数据。它使用一个秘钥和初始化向量(某个方向的数量)对数据进行加密转换。能有效的保护密文的完整性,如果一个数据发生改变,后面所有的数据都会被破坏。

现代的密码学都和几何有关,因为几何(圆形/椭圆/球体等)的变量是有规律的,但是结果又是多变的。

4.2 ECB 和 CBC 区别

这里我们通过一个示例,来看一下 ECB 和 CBC 的区别。
先建立一个 txt 文件,文件内容随意。

ECB 加密(DES)

使用下面的指令,对刚才的 txt 文件使用 ECB 加密:

openssl enc -des-ecb -K 616263 -nosalt -in gof.txt -out gof.bin

然后通过指令 xxd 来查看 gof.bin 的十六进制:

xxd gof.bin

结果如下图所示:

这时我们稍微改一下 txt 文件,修改最后一排的某个字符即可。然后再进行加密,查看结果:

可以看到仅上面红框中的内容有改变。

CBC 加密(DES)

使使用下面的指令,对刚才的 txt 文件使用 CBC 加密:

openssl enc -des-cbc -iv 0102030405060708 -K 616263 -nosalt -in gof.txt -out gof3.bin

我们稍微改一下 txt 文件,修改最后一排的某个字符即可。然后再进行加密,对比两次 CBC 加密的结果:

4.3 小示例:对称算法应用

```
/// AES - ECB 加解密

  • (void)encrytionAESWithECB {
top Created with Sketch.