本文来自《图解密码技术》一书

基本概念

  • 信源、信宿、信道
  • 加密、解密、密钥
  • 对称密码、公钥密码、混合密码
  • 单向散列(hash)函数、信息认证
  • 数字签名 / 篡改和否认
  • 伪随机数

信息传递时面临的风险:

  • 窃听 -> 对称、公钥密码
  • 篡改 -> 散列函数、消息认证、数字签名
  • 伪装 -> 消息认证、数字签名
  • 否认 -> 数字签名

有悖常识的几点:

  • 不要使用保密的加密算法
  • 使用低强度密码不如不用
  • 任何密码都有可能被破解
  • 密码只是安全的一部分(社工)

入门

  • 凯撒密码 / 平移 / 暴力破解
  • 简单替换密码 / 替换 / 频率分析
  • Enigma / 加密密码的密码 / 针对每日密钥的破解

对称密码

  • 编码和异或
  • 一次性密码和它的问题
  • 使用对称密码时,我们通常默认密钥配送问题已解决

DES

全称Data Encryption Standard。于1977年在美国发明并使用。目前可以被暴力破解,因此不应再使用了。

  • DES以64bit为一个单位,使用和明文等长的密钥。
  • 密钥每8位包含1位纠错码
  • 基本结构使用Feistel网络
    • 加密/解密步骤以轮为单位,DES有16轮
    • 每轮运算不加密右半侧,同时右半侧比特和该轮密钥通过轮函数得到本轮加密左侧的密钥,和左侧做异或得到左侧密文
    • 每轮加密后,进行左右对调,保证右侧的保密
    • 解密时用相同结构,反向使用子密钥和轮函数即可
    • 轮函数可以任意复杂
  • 差分分析和线性分析衡量分组密码强度

三重DES

由IBM开发,步骤为DES加密 -> DES解密 -> DES加密。密钥长度是原来三倍,即168比特。

  • 三步使用同一密钥,即向下兼容DES
  • 根据1、3步是否使用同一密钥,分为DES-EDE2和DES-EDE3
  • 处理速度慢

AES

全称Advanced Encrytion Standard,用来取代DES。由NIST开于1997年开始募集,将作为国家标准。算法要求开源免费,并在社群里公开评审,最终于2000年确定为Rijndael。

  • 基本结构为SPN结构
  • 明文分组长度固定为128bit、密钥长度可以是128、192、256比特三种
  • 每轮分为SubBytes、ShiftRows、MixColumns、AddRoundKey四步
    • SubBytes,将4字节 * 4字节的矩阵与一个转换矩阵相乘,得到替换后的矩阵
    • ShiftRows,逐行做平移
    • MixColumns,逐列做矩阵运算
    • AddRoundKey,和轮密钥矩阵做对应位上的异或运算
  • 解密时,除了AddRoundKey一步,其余均需要做逆运算
  • 目前还没有针对Rijndael的有效攻击
  • 避免使用DES,选择AES

分组密码的迭代模式

分组密码只能加密固定长度的密码。因此,需要有模式来迭代加密任意长度的明文。与分组密码相对的是流密码,对数据进行连续处理。

ECB

全称为Electronic CodeBook。是最简单直接的分组方式。将明文分组加密后直接得到对应位置的密文。不足的位用特定数据填充

  • 问题很明显,不要使用
  • 相同的明文分组会得到相同的密文分组
  • 攻击者无需破译密码也可通过修改密文操纵明文,比如替换或删除分组

CBC

全称Cipher Block Chaining。和ECB的最大不同在于明文分组加密前会和上一个密文分组做一次异或运算。开头的明文分组会和一个随机序列做XOR。

  • 一个密文分组的损坏会影响关联的两个分组的解密
  • 比特的缺失将会影响整个密文的解密
  • 操纵初始化向量反转某一位比特可以反转第一个密文分组的某一位
  • 填充提示攻击,攻击padding部分
  • SSL/TLS使用CBC模式保证通信机密
  • CTS使用最后一个密文填充不足的明文部分

CFB

全程Cipher FeedBack模式。和CBC模式的区别在密文分组先加密,再和下一个明文做异或运算。实际上明文分组和密文分组间只相差了一个异或运算。很类似一次性密码本的模式。

  • 解密时,需要对密文分组和初始向量做加密操作
  • 重放攻击,替换密文分组为原有分组,可使得解密出的明文为原有明文

OFB

全称Output-FeedBack模式。和CFB很像,区别在于OFB每次做XOR的密钥流仅来自于上一次的密钥,和密文分组无关。因为密钥流可以提前准备好,分组加密过程可以是并行的。

  • 第一次的密钥来自初始化向量
  • 速度快
  • 如果碰巧密钥加密后和加密前一样,那么之后的密钥就会是同一个值

CTR

全称CounTeR。CTR和OFB类似,区别在于它的密钥流来自于累加的计数器。密文分组来自于密钥流和明文分组的XOR运算。

  • 计数器由nonce和序号两部分各8字节组成,nonce是随机生成的,序号是从1累加的。
  • 和OFB一样,加密解密速度快,结构简单
  • CTR的密钥流在选定nonce后就确定了,因此可以以任意顺序并行加密、解密

公钥密码

解决了对称密码的密钥配送问题。

密钥配送问题

  • 事先共享,在现实生活中传送
  • 密钥配送中心,集中式管理用户密钥,用其加密临时的会话密钥
  • 使用Diffie-Hellman密钥交换
  • 使用公钥密码

公钥密码

使用加密密钥(公钥)加密,使用解密密钥(私钥)解密,避免密钥的泄露。

  • 发送者使用加密密钥
  • 接收者使用解密密钥
  • 加密密钥可以公开
  • 解密密钥一定要保密

目前所使用的公钥密码RSA来自于1978年的发明。流程上,

  • 接收者生成公私钥对,发送公钥给发送者
  • 发送者使用公钥加密明文
  • 接收者使用私钥解密密文

公钥密码有两个问题:

  • 认证公钥的合法性
  • 处理速度慢

RSA

利用了数论中求解离散对数困难且耗时的特点。

  • 加密,使用密文=明文 ^ E mod N。E和N组合成公钥。
  • 解密,使用明文=密文 ^ D mod N。D和N组合成密钥。

生成N、E、D和顺序如下:

  1. 寻找互质的两个大数p和q,N为二者的乘积
  2. p-1和q-1的最小公倍数记为L
  3. 寻找比L小的和L互质的数,即为E
  4. 寻找比L小的和E乘积取模L为1的数,即为D

因为解密时有对N取模操作,因此加密的明文不能大于N。

攻击方式

  • 破解密文 -> 求解离散对数很难
  • 暴力破解D -> 比特位太长,很难破解
  • 通过E求解D,只要知道p和q就能算出D -> 不知道p和q的组合 -> 对N质因数分解很难

中间人攻击里,攻击者可以替换掉原本的公钥,发送给接收者,使用自己的私钥解密,从而实现攻击。这时需要证书保证公钥的权威性。

选择密文攻击里,攻击者可以利用服务端返回的错误消息收集加密算法信息。RSA-OAEP会在明文开头加上明文散列值和填充位,解密时发现散列值和内容对不上时,会隐藏错误信息。

除了RSA外,还有ElGamal方式、Robin方式、ECC(椭圆曲线密码)等公钥密码。它们分别利用了mod N下求离散对数,mod N下求平方根,和椭圆曲线上做乘法运算逆运算在数学上很难求解的特点。

FAQ

Q: 和对称密码的强度对比
A: 达到同等强度,RSA大致需要密钥是AES长度的20倍

Q: RSA使用的质数会用完么
A: 512bit的质数数目大约是10 ^ 150。足够使用。

Q: RSA破解难度如何?
A: 和大整数质因数分解一样难度

Q: 要保证RSA强度,N的长度要达到多少位
A: 2048bit,4096bit更好

混合密码系统

  • 用对称密码加密明文
  • 用公钥密码加密上述对称密码的密钥(通常用随机数生成器得到,只用于此次会话)
  • 公钥密码的密钥由外部赋予(证书)

密码软件PGP、HTTPS中使用的SSL/TLS就使用了混合密码系统。当然它们还包含数字签名、认证、私钥管理等更多处理。

类似混合密码系统,后面要介绍的数字签名、证书、消息认证、伪随机数生成也都是基础密码技术的组合

单向散列函数

  • 将任意长度的消息转换到固定长度散列
  • 具有抗碰撞性,即找到具有相同散列函数的消息很困难
  • 单向性,即无法从三列中还原原信息

MD4、MD5

全称Message Digest。由Rivest设计于1990和1991年。能够产生128bit的散列值。它们的强抗碰撞性已被攻破,不建议使用

RIPEMD-160

1996年设计,是欧盟RIPE项目的修订版,能产生160bit长度的散列值。比特币中使用的散列函数就是RIPEMD-160。

SHA

SHA于1993年由NIST设计,在1995年发布了SHA-1修订版,能够产生160bit的散列值。它的强抗碰撞性已被攻破,也不建议使用

SHA-2于2002年发布,它是包括SHA-256,SHA-384和SHA-512的集合,分别产生256、384和512bit的散列值。目前未被攻破。SHA-2的几种散列长度来自SHA-256和SHA-512的组合。

SHA-3作为SHA-1的升级替代算法,和AES一样,由NIST公开选拔,并在2012年确定为一个叫Keccak的算法。之后会和SHA-2并存一段时间。

Keccak

Keccak可以输入任意长度的数据,产生任意长度的散列值。实现上,Keccak采用海绵结构,有吸收挤出两阶段。

  • 吸收阶段,按分组长度r逐段读入消息内容,和内部状态做异或运算,之后和长度为c的内部状态一起交给函数f做“搅拌”。完成一轮处理,输出作为内部状态继读入输入的消息分组。
  • 挤出阶段,内部消息r和c逐段和函数f做运算,一段段输出散列值。

Keccak的双工结构下,输入和输出可以同时进行。Keccak内部状态由5 * 5 * z的一个三维比特数组组成,共有b个bit。Keccak的本质就是实现一个充分搅拌上述数组的函数f。SHA-3中使用的是Keccak-f[1600]函数。其中b就是内部状态的bit数。函数的每一轮包含θ、ρ、π、χ、ι5步。循环轮数为12 + 2 * log2(b / 25)。

  • θ,将不同两个column的各5个bit通过异或运算加起来,再和当前位做异或替换
  • ρ,各比特沿z轴方向进行平移
  • π,对一个slice上的5 * 5个比特做旋转、轮换操作
  • χ,对一个row上的各位做某个逻辑运算
  • ι,用某个轮常数对所有比特做异或运算,避免对称性

Keccak采用的海绵结构和此前各散列算法使用的MD结构(循环执行压缩函数)方法截然不同,这也是它最后成为标准的一个原因。目前还未出现针对Keccak的有效攻击手段。

攻击方式

利用文件的冗余性,构造一大堆和想要内容一样的数据,找到和原内容散列值一样的结果。

  • 原像攻击,给定散列值,找到具有该散列值的任意消息
  • 第二原像攻击,给定消息1,找到和消息1有相同散列值的消息2
  • 生日攻击,攻击散列算法的“强抗碰撞性”(寻找两个具有相同散列值的消息),利用了从有N个元素的集合中依次取并放回M个元素,两次取到同一元素的概率约为根号N的特点。大大减少暴力破解需要的次数。

消息认证

可以同时防止消息的伪装和篡改。消息认证码简称MAC(Message Authentication Code)。可以简单理解成需要密钥参与的单向散列过程。在使用时:

  • 发送者伴随消息发送计算出的MAC
  • 接受者对消息通过共享密钥计算出MAC值,进行对比,一致则表示认证成功
  • 这个密钥不能被中间人获取!

使用消息认证码(MAC)机制的场景有:

  • SWIFT
  • IPSec
  • SSL/TLS

在认证加密时,Encrypt-then-MAC表示对密文计算MAC值,从而能判断密文是由知道明文和密钥的人生成的。除了Encrypt-then-MAC外,还有Encrypt-and-MAC和MAC-then-Encrypt两种方式。

HMAC

HMAC即Hash MAC,是使用单向散列函数构造认证码的方法。分为下面几步:

  1. 在密钥后填充0到长度达到单向散列函数的分组长度
  2. 填充后的密钥和ipad序列做XOR运算,ipad序列是00110110为单位循环的比特序列
  3. 组合在消息头部,并计算出散列值
  4. 填充后的密钥和opad做XOR运算,opad是01011100位单位循环的比特序列
  5. 结果拼在散列值后面
  6. 根据5的结果计算最终的散列值

应对攻击方式

  • 消息认证需要解决重放攻击的问题,即再次发送相同的消息和MAC值。可以在消息中额外带上序号、时间戳,或先发送一个nonce一次性随机数保证相同的消息也会有完全不同的MAC值。
  • 密钥推测攻击,应保证不能根据MAC值推测出双方使用的密钥,必须使用安全、高强度的伪随机数生成器。

另外,消息认证无法解决下面的问题:

  • 向第三方证明,密钥的共享只在通信的双方,无法证明给第三方
  • 同样的,不能防止通信的一方否认消息

数字签名

和公钥密码相反的使用方式:

  • 发布者使用私钥加密消息,私钥保密
  • 使用发布者的公钥可以解密消息,公钥公开

签名有两种方式:对消息签名对消息的散列值签名。它们主要区别在签名的对象不同。基本过程是:

  1. 生成公、私钥对,发送公钥给接收者
  2. 使用私钥加密消息/消息的hash值,得到签名
  3. 发送消息和签名给接收者
  4. 接收者使用公钥解密,对比消息/消息hash值,验证发送者身份

在签名中,密钥只是起着“保证消息发送者的可靠来源目的的”,被复制并不影响它发挥作用。同时,由于不知道私钥,修改消息后无法伪造消息的签名。

实际应用数字签名的地方有很多:

  • 安全信息公告
  • 软件下载
  • 公钥证书,确保公钥的合法来源
  • SSL/TLS,交换公钥的过程

数字签名基于公钥密码,因此数字签名的实现方式因采用的公钥密码而异,如RSA、ElGamal、ECDSA(椭圆曲线密码)。对数字签名的攻击可以基于单向散列函数或是公钥密码。

  • 不要对不清楚来源的数据做数字签名
  • 对消息的散列值函数做数字签名

数字签名无法解决验证签名正确性的公钥被伪造的问题,因为公钥正确性也依赖于数字签名技术。这里需要证书以及公钥基础设施PKI这种社会学的基础设施辅助。

证书

证书即公钥证书,用来验证公钥密码和数字签名的公钥,由认证机构(CA)发布,认证机构可以是政府机关、一般企业或个人。证书的发布过程包括:

  1. 申请人生成一对密钥,并把公钥发送给CA
  2. CA验证申请人身份
  3. 通过验证后,CA使用自己的私钥对公钥施加数字签名并生成证书
  4. 使用申请人证书的使用者通过CA的公钥验证申请人的公钥是否合法
  5. 验证通过后,使用公钥完成公钥密码或数字签名

PKI是为了能够更有效运用公钥制定的一系列规范的总称。PKI组成要素有3个:使用PKI的用户、认证机构、仓库。

  • 用户,分为注册公钥的用户和使用注册公钥的用户
  • CA,包括生成密钥、验证本人身份、验证公钥合法性、作废证书
  • 仓库,是保存证书的数据库

其中认证机构做了以下事情:

  • 生成密钥对,可以由用户或是CA生成,若是CA生成,需要根据规范发送私钥给用户
  • 注册证书,用户根据规范申请证书,认证机构根据业务准则生成符合X.509规范的证书
  • 作废证书,因为私钥丢失等原因需要作废证书时,需要认证机构制作CRL(Certificate Revocation List,证书作废清单),PKI用户总需要从CA获取最新的CRL,以确认自己拿到的公钥证书是否有效。

认证机构的证书认证

认证机构的公钥证书可以由其他的认证机构施加数字签名。这个关系可以嵌套很多层,比如部门认证机构、分公司认证机构、总公司认证机构。一直往上直到根CA,可以对自己的公钥做自签名。

从而,在验证证书合法性上,也会出现从上至下的验证过程。

证书的攻击

对证书的攻击即对数字签名的攻击。

  • 对施加数字签名前的公钥攻击
  • 注册相似人名进行攻击
  • 窃取CA的私钥
  • 伪装成CA发放证书进行攻击,认证机构本身的可信度也很重要
  • 利用发送CRL的时间间隔,窃取了使用者的私钥,当使用者联系CA发布CRL时,有一定的时间间隔
  • 同样利用CRL,使用合法私钥发送消息后,发送CRL作废自己的公钥,否认自己之前发送的消息

不可能在完全不可信的状态下创建出信任关系,除非以已经存在的信任关系为基础。

密钥

  • 密钥长度(DES:56bit,三重DES:112bit或168bit,AES:128、192、256bit

  • 对称密码和公钥密码用于确保机密性,消息认证码和数字签名使用的密码用于认证,防止篡改内容和伪装身份

  • 只使用一次的密钥称为会话密钥,重复使用的密钥称为主密钥

  • 密码学用途的随机数生成器必须为密码学用途专门设计

  • 定期改变会话密钥可以减少密钥泄露的损失

  • 保存密钥时,使用KEK(Key Encrypting Key)方式保存密钥可以减少管理密钥的数目。

Diffie-Hellman密钥交换

Diffie-Hellman密钥交换里,通信的双方通过交换一些可以公开的消息,就能够生成共享的密钥。

  1. 确定一个非常大的质数P,寻找P的生成元(原根)G
  2. 通信双方各自找1个1 ~ P-2的随机数A、B,生成G ^ A mod P与G ^ B mod P,发送给对方
  3. 对方用收到的数字根据自己选的随机数做乘方运算,得到相等的值作为密钥

它同样利用了离散对数问题难以快速求解的特点。这种交换方法可以做中间人攻击,可以用数字签名、证书等方式应对。

基于口令的密码(PBE)

基于口令的密码避免了:记忆CEK -> 记忆KEK -> 记忆KEK的KEK的死循环。使用好记忆的口令配合盐生成CEK。使用过程如下:

  1. 使用随机数生成器生成盐(随机数),加上用户口令,使用单向散列函数得到KEK
  2. 使用KEK加密会话使用的CEK
  3. 保存好盐以及使用KEK加密的会话秘钥
  • 盐的目的是避免字典攻击
  • 口令虽然便于生成,但是强度不高,因此需要格外地小心保管
  • 可以对KEK迭代使用单向散列函数得到最后的KEK(拉伸),这将加大攻击者的攻击负担

生成安全的口令

  • 使用只有自己知道的信息
    • 不包括别人见过的信息
    • 不包括可以很容易推测的信息
  • 不应该重复使用口令,容易受牵连影响
  • 物理保存是可以的,但要注意安全
  • 可以使用口令生成和管理工具(比如1Password)

随机数生成

随机数生成在密码学中很常用:

  • 生成密钥
  • 生成分组密码的初始化向量
  • 生成CTR模式的nonce
  • 生成盐

随机数至少需要具有下面的属性:

  • 随机等概性
  • 无状态,即无法从上一个推测下一个,生成序列无法重现

由于计算机构成的抽象世界是离散的,内部状态有限,不能满足无状态的特点,因此只能称作伪随机数生成器。基于计算机硬件的随机数生成器可以认为是“真”随机数,它通常提前储存在一个随机数池中,在需要的时候直接从池中取用。伪随机数生成器根据随机的种子(seed)通过算法将内部状态转化为最终的随机数。

  • 线性同余法,以当前随机数为内部状态(初始值为种子),(A x Rn + C) mod M,计算下一个值。其中A、C、M都需要事先选好,线性同余法生成的随机数数列具有可预测性,即不需要知道种子也可以推测下随机数值
  • 单向散列函数,利用单向散列函数保护内部状态,以种子为初始值,逐次递加得到新的内部状态,再通过单向散列函数输出为随机数
  • 密码法,类似单向散列函数,使用密钥加密内部状态输出也可以作为随机数,此时保护内部状态的加密算法和密钥
  • ANSI X9.17中,使用AES和三重DES作为密码算法
    1. 初始化内部状态
    2. 使用当前时间生成掩码
    3. 掩码和内部状态做XOR
    4. 加密3的输出,作为随机数输出
    5. 对加密后的输出与掩码做XOR
    6. 加密5的结果作为新的内部状态

PGP介绍

PGP全程Pretty Good Privacy,编写于1990年,具备现代密码软件所需的几乎所有功能。OpenPGP是一对密文和数字签名进行定义的标准规格。

加密和解密

加密时,使用混合密码系统的流程:

  1. 用伪随机数生成会话密钥
  2. 接收者的公钥加密会话密钥
  3. 压缩消息,并使用对称密码加密,密钥为上面生成的会话密钥
  4. 将加密后的密钥和密文拼接在一起
  5. 将4的结果转换为文本数据,即为报文数据

解密时,PGP的私钥通过用户口令加密保存。在收到密文时:

  1. 输入接收者的口令
  2. 求口令的散列值,生成用户解密私钥的秘钥
  3. 解密得到私钥
  4. 将报文数据转换为二进制,并拆解成加密的会话密钥和压缩的密文
  5. 用自己的私钥解密得到会话密钥
  6. 用会话密钥解密密文
  7. 解压缩明文得到原始消息

生成数字签名

同样,生成数字签名时:

  1. 输入接收者的口令
  2. 求口令的散列值,生成用户解密私钥的秘钥
  3. 解密得到私钥
  4. 使用单向散列函数计算消息散列值
  5. 对散列值签名,即使用私钥加密
  6. 拼合签名和消息,进行压缩
  7. (可选)转换二进制为文本数据,即最后的报文数据

类似地,验证时:

  1. 转换为二进制文件,解压缩数据
  2. 分解出签名和消息两部分
  3. 使用公钥解密签名,得到散列值
  4. 使用单向散列函数计算消息散列值,对比3中的散列值
  5. 相等即验证成功

生成数字签名并加密

实际情况下,我们往往需要使用加密算法加密数字签名中的原消息。实现步骤是上两节的组合。即先进行数字签名,再对签名结果加密。

验证过程是相反的,先解密密文得到签名结果,再验证数字签名。

信任网

PGP确认公钥合法性的方法不依赖于认证机构颁发证书,而是采用所有者信任级别构成信任网(也叫信任圈、朋友圈)的方式,让用户自己决定该信任谁。建立信任有三种方式:

  • 通过自己的签名来确认。用户在通过其他方式(比如线下)确认公钥可信任后,对该公钥加上自己的数字签名。由于PGP中,使用者本人的公钥是绝对信任,被施加签名的公钥因此可信任。注意:这并不代表被施加签名的公钥所有者被完全信任
  • 通过自己完全信任的数字签名进行确认。即完全信任某个公钥进行的数字签名,用户可对当前信任的每个公钥所有者设置信任级别,级别为完全信任时,所有者公钥施加签名的公钥也会被信任。
  • 通过有限信任的多个数字签名进行确认。在设置信任级别为有限信任时,有限信任的公钥施加数字签名后,新的公钥才会被信任。

通过上面三种方式,PGP使用者可以构建起自己的信任网,从而根据自己的决定信任某个公钥。

SSL/TLS

TLS是SSL的后续版本,但在大多数情况下,可以统一写成SSL/TLS。SSL/TLS可以承载应用层协议,保证应用层传输的安全性,HTTP就是其中一种。其余SSL/TLS可以承载的应用层协议还包括SMTP、POP3等等。

SSL于1994年在网景公司开发,在1995年发布了SSL3.0版本,后被发现会导致POODLE攻击。TLS是IETF在1999年作为SSL3.1发布。2006年发布TLS1.1,之后又发布了TLS1.2。

HTTPS中SSL/TLS要保证以下三点:

  • 保证消息传输中不被窃听 -> 对称密码加密消息,公钥密码加密对称密码的密钥
  • 保证消息传输中不被篡改 -> 消息认证
  • 保证消息传输双方的合法性 -> 数字签名生成证书

通信过程

下面的流程以TLS1.2为例。TLS协议分为两层:

  • TLS握手协议,位于上层,处理除加密的部分。可以进一步分为:
    • 握手协议,负责在客户端和服务器间协商密码算法和共享密钥
    • 密码规格变更协议,向通信对象传达变更密码方式
    • 警告协议,在发生错误时将错误传达给对方
    • 应用数据协议,将TLS上承载的应用数据传达给通信对象
  • TLS记录协议,位于底层,处理加密的部分。使用了对称密码和消息认证码,但具体的算法和密钥需要通信双方具体协商

TLS记录协议

记录协议负责数据的压缩、加密、数据认证,工作方式如下:

  1. 分割消息为较小的片段,再分段压缩,压缩方式需要协商决定
  2. 对压缩过的消息进行消息认证,加上MAC值。为了避免重放攻击,在计算MAC值时,加上了片段的编号。其中的单向散列函数的算法、使用的密钥都需要协商确定
  3. 把MAC值和压缩过的消息片段组合在一起,使用对称密码加密。迭代模式使用CBC模式,CBC模式的初始化向量通过主密码生成。对称密码的算法、密钥则需要协商决定
  4. 上述经过加密的数据,再加上数据类型、版本号、压缩后的长度,构成最终的报文数据。数据类型就是之前提到的TLS握手协议的4类子协议

握手协议

握手协议负责生成对称密码中的共享密钥以及交换证书。因为握手的整个过程都是明文进行的,因此需要使用公钥密码或是Diffie-Hellman密钥交换。整个握手协议有下面几步:

  1. ClientHello,客户端发送一些信息给服务器,便于协商算法和密钥
  • 可用版本号,即支持的SSL/TLS版本号
  • 客户端生成的随机数,在后面的步骤会用到
  • 会话ID,在需要重新使用以前的会话时用到
  • 客户端可用的密码套件清单
  • 客户端可用的压缩方式清单
  • 当前时间
  1. ServerHello,服务器根据客户端传来的信息,选择合适的算法和密码套件,返回的消息中带有下面几条
  • 使用的版本号
  • 服务端生成的随机数,后面步骤会用到
  • 会话ID,作用同上
  • 使用的密码套件
  • 使用的压缩方式
  • 当前时间
  1. Certificate非匿名通信时,服务器发送自己的证书,以及对服务器证书签名的CA的证书
  2. ServerKeyExchange,当Certificate消息不足时,服务器通过此消息传递额外信息
  3. CertificateRequest,需要进行客户端认证时,服务端发送此消息,并带上服务器能理解的证书类型、CA名称清单。
  4. ServerHelloDone,服务器发送此消息结束服务器的返回
  5. Certificate,作为CertificateRequest的回应,客户端发送自己的证书,交给服务器验证
  6. ClientKeyExchange,密码套件包含RSA时,会发送经过服务器公钥加密的预备主密码;密码套件包含Diffie-Hellman密钥交换时,会发送Diffie-Hellman密钥交换中的公开值。预备主密码(pre-master secret)是客户端生成的随机数,之后会用做生成主密码的种子。根据预备主密码,通信双方计算出相同的主密码。主密码会用做以下用途:
  • 对称密码的密钥
  • 消息认证码的密钥
  • CBC模式中的初始化向量
  1. CertificateVerify,在服务器发送CertificateRequest时,通过此消息发送客户端使用自己私钥签名的主密码和握手协议传输消息的散列值。证明自己是客户端证书的持有人。
  2. ChangeCipherSpec,客户端发送,表示切换密码开始,实际上是密码规格变更协议的一类报文
  3. Finished,握手结束,此时已使用切换后的密码套件来加密发送。
  4. ChangeCipherSpecFinished。来自服务器,作用同上。

通过上面的步骤,双方达成了下面的目标:

  • 客户端获得了服务器的公钥,完成了服务器认证
  • 服务器获得了客户端公钥,完成了客户端认证(如果需要的话)
  • 生成了对称密码的密钥
  • 生成了消息认证码中的共享密钥

密码规格变更协议

用于在一开始从明文通信切换到使用密码套件沟通。

警告协议

用在握手协议异常、消息认证码错误、无法解压数据等异常情况。

应用数据协议

通信对象间传递应用数据。

主密码

主密码根据预备主密码(pre-master secret)或Diffie-Hellman密钥交换的公开值生成。生成的主密码用于生成对称密码的密钥、消息认证码的密钥、CBC模式的初始化向量

对SSL/TLS的攻击

  • SSL/TLS框架性的特点让它不依赖于某个特定的密码技术,因此对特定密码技术的攻击对SSL/TLS本身影响不大
  • 心脏出血漏洞,发现于2014年,利用TLS心跳拓展对请求的数据大小没有检查,可以获取内存中与请求无关的信息。是OpenSSL实现的漏洞。
  • POODLE攻击,利用CBC中的填充提示攻击,发现于2014年SSL3.0中。
  • FREAK攻击,可以在密码套件协商时,利用中间人攻击,强制使用强度很低的RSA Export Suites。从而在加密后,暴力破解明文。
  • 对伪随机数生成器的攻击
  • 利用之前提过的CRL

总结

密码技术因为人类的不完美而必定不会完美。

  • 对称密码,使用相同密钥加密、解密,保证消息机密性。目前主要使用AES。
  • 公钥密码,使用不同密钥加密、解密,作用同上。使用最广泛的是RSA,还有相关的Diffie-Hellman密钥交换
  • 单向散列函数,将消息转为固定长度散列值的技术,保证消息完整性,目前使用SHA2和SHA3(Keccak)
  • 消息认证码,结合单向散列函数和对称密码,保证消息完整性认证消息,但无法防御否认。目前主要使用HMAC
  • 数字签名,结合单向散列函数和公钥秘钥,保证完整性不可否认性认证消息。是公钥证书采用的技术
  • 伪随机数生成器,配合上述技术使用,需要保证不可预测性不可重现性

密码技术从某种角度看是一种压缩技术:

  • 密钥是机密性的压缩
  • 散列值是消息完整性的压缩
  • 认证值时认证的压缩
  • 随机数种子是不可预测性的压缩

比特币

比特币来自于Satoshi Nakamoto(中本聪,化名)的一篇论文,并于2009年开始实际运用。比特币是一种基于P2P网络的支付结算系统。用户通过它进行进行价值转移。

  • 地址,将公钥使用散列函数求散列值得到,地址都以1开头,剩下内容 不包含O,0,1和I。
  • 钱包,即比特币客户端,可以生成密钥对,公钥用于收款,密钥用于付款
  • 区块链,保存了比特币所有交易记录的账簿,若干交易组成一个区块,在区块头有所有交易的散列值,以及上一个区块的散列值,有交易添加时会触发区块头的散列值变化,并链式传递下去
  • 交易,收、付款方各自生成密钥对,付款方创建交易“地址A向地址B转账x BTC”,并用自己的私钥签署数字签名,之后广播至P2P网络中,完成交易。比特币使用的数字签名基于椭圆曲线DSA,方程为x^2 = y^3 + 7
  • 挖矿,向区块链中添加新区块的行为被称为挖矿,第一个挖矿成功的矿工会获得挖矿奖励和区块所有交易的手续费。为了证明自己确实完成了规定工作,矿工需要进行工作量证明(PoW),即生成的区块头中,前一区块头的散列值必须以若干位的0开头,这个工作需要投入大量的计算资源。区块大约每10分钟添加一个,为了避免通货膨胀,所需的0的个数会不断调整。
    • 根据协议规定,挖矿奖励每4年减少一半
    • 当区块链上同时出现分支时,P2P网络会选择计算量大的分支进行工作
  • 比特币的匿名性只限于交易地址

附录:椭圆曲线

  • 椭圆曲线(EC)源自于求椭圆弧长的椭圆积分的反函数。
  • 定义椭圆曲线上的加法运算
  • 椭圆曲线上的离散对数(ECDLP) - 已知点G和点xG,求整数x
  • 有限域上的离散对数,对点进行模运算
  • 椭圆曲线Diffie-Hellman密钥交换
  • 椭圆曲线ElGamal密码
  • 椭圆曲线DSA(ECDSA)

lottie production环境下bug修复总结

现象:
前段时间,用lottie-web做动画的时候,发现在有个别动画在本地测试时可以正常播放,打包上线后会报库代码内的错误

猜测原因:
打包过程中的uglify有损压缩了lottie-web的代码,导致部分特性的bug

修复方式:

  1. 在webpack配置中,为lottie-web专门指定一个chunk
  2. 在optimization中,指定一个lottie的cacheGroup,保证一个专门的chunk
  3. minimizer中uglifyJSConfig指定exclude为lottie的chunk名,避免被uglify
  4. resolve中,指定lottie-web resolve到’../node_modules/lottie-web/build/player/lottie.min.js’,使用压缩过的版本

大致像下面这样:

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
module.exports = {
entry: {
// ...
lottie: ['lottie-web'],
// ...
},
// ...
resolve: {
alias: {
// ...
'lottie-web': path.join(__dirname, '../node_modules/lottie-web/build/player/lottie.min.js')
}
},
// ...
optimization: {
splitChunks: {
cacheGroups: {
lottie: {
chunks: 'initial',
name: 'lottie',
test: 'lottie',
enforce: true
}
}
},
minimizer: [
new UglifyJsPlugin({
exclude: /lottie/,
})
]
}
};

结果:
问题解决。

Android机型下rem适配不准的问题

参考 https://www.jianshu.com/p/14f6ce51a75f

通过比较document.documentElement.style.fontSizewindow.getComputedStyle(document.documentElement)['font-size'],假设前者是a,后者是b,a * a / b计算得到和设计一致的尺寸。

3D旋转效果

利用CSS中的backface-visibility: hidden;属性,实现在transform: rotateY(180deg)时,页面翻转到不可见区域。

egret学习

场景:H5小游戏开发

投放场景:各种小游戏平台,也可以打包为Android、iOS、Windows Phone应用发布,甚至直接web访问H5页面

开发语言:TypeScript

开发方式:

  • 类Java的代码组织方式,MVC分离项目代码,M和C开发体验类似“用JavaScript写Java项目”。在View部分开发体验类似于用canvas API写页面结构
  • 类Android的resource管理方式(定义json文件描述资源组和路径对应),在代码中用API动态分组或逐个load资源
  • 单一入口,流程上在stage加载完成后,load资源(同时给出loading页面),之后执行游戏逻辑
  • 使用dispatchEvent实现组件间的信息交流

项目结构

入口文件为index.html。在其中引入manifest.json。读入所需的库文件后,根据DOM容器的data-*属性确定项目配置,以及项目入口*.ts(一般是Main.ts)。之后打包编译到bin-debug

业务逻辑放在/src下,资源文件放在/resources中,资源文件用类似于Android的形式进行存取管理。

视图

Displayable元素可以添加到容器中显示。包含下面基类。

  • displayableContainer 视图元素容器
    • stage
    • scrollView
    • sprite
  • bitmap
  • bitmapText
  • textField
  • movieClip
  • shape

movieClip表示逐帧动画。生成方法如下:

  1. RES.getRes获取资源
  2. 使用factory方法构造movieClipData
  3. 使用movieClipData构造movieClip

简单动画用tween来实现。

发布

  • egret publish或run build。发布H5,runtime版本
  • 对应平台support工具,如Android、iOS、微信小程序

不过在最新的egret launcher下,项目本身已经提供的发布到原生的快捷入口,参考官方解释

难点

和React如何结合开发?

View层通过canvas、WebGL实现,不适合和React结合。

部署方式如何结合在App里

小游戏可以发布到HTML5平台,之后类似老的webview页面开发方式,部署到离线包平台或在线页面即可。

SSO实现方案

SSO - Single Sign On 单一站点登录。由一个站点的登录状态实现关联网站免登录。

背景

由sso.xxx.com记录用户登录态,其他需要使用同一登录态的网站需要同步该域名下的登录态cookie到自己的独立域名下。

实际场景

一般公司内部的网站或ToC的集团网页间都有SSO控制,任意访问一个清除了所有cookie网页,观察network中开头的302报文即可发现实现SSO过程中的各跳转逻辑。

实际步骤因实现而异:

  1. (转让控制权)访问目标网页,302到SSO的跳转特定页面,如jump.sso.xxx.com
  2. (写入cookie)302回目标网页的特定页面,如sso.mysite.com。该域名CNAME到sso.xxx.com的服务器
  3. (写入cookie)sso.mysite.com写入cookie到自己的同域名下,再次302到目标网页,完成SSO过程

或者

  1. 同上
  2. (写入cookie)jump.sso.xxx.com做cookie的检查确认,通过url的方式写入回调的user session,再302回mysite.com。
  3. (写入cookie)mysite.com的后台对应路由根据URL里的回调写入cookie,302到目标页面

在写入cookie到新域名过程中,可以有不同的实现方式。

原理

第一步302到sso.xxx.com的时候已经可以带上xxx.com的cookie了,但是由于浏览器安全限制,并不能直接set cookie到独立域名下。需要再次302回原始域名,CNAME到sso的服务器,实现set cookie到独立域名。

为了保证安全性,CNAME到sso的sso.mysite.com所传递的参数需要有安全机制保证。如时间戳、秘钥等保证请求的完整性。避免中间人伪造域名下的请求。同时,链接本身也应有时效性,在超过时间范围失效,避免拦截链接,实现钓鱼网站获取sso.xxx.com的登录态。

具体步骤:

  1. 302到jump.sso.xxx.com后,进行权限检查判断域名是否允许同步,匹配SSO的cookie域名下的cookie取交集,得到需要同步的cookie。
  2. 通过以上两步后,302到sso.mysite.com,url中带上cookie和安全相关的参数
  3. 根据安全参数校验、target是否允许同步,决定返回403还是302。
  4. 通过校验后,同步登录态cookie,302到目标网页

清除登录态时,如何做到相关域名的同时清除,还需要额外设计。

当然,如果sso只在内网使用,在jump.sso.xxx.com做完安全验证后,通过url将结果交由sso.mysite.com设置登录态Cookie,要更为简洁。

git branch rename

如果分支在远端也有的话,工作需要分为本地和远端两部分。

  1. 重命名本地分支
  • 如果就在该分支
    1
    git branch -m new-name
  • 如果在其他分支
    1
    git branch -m old-name new-name
  1. 删除原分支,推送新分支
    1
    git push origin :old-name new-name
  2. 重置upstream设置
    1
    git push origin -u new-name

webpack无痛mock方案

使用webpack-api-mocker实现,对比axios-mock-adapter和其余方案有几个优势:

  • mock部分代码和业务代码分离开,让网络请求部分代码(/apis)有清晰的逻辑,不混杂业务无关内容
  • 热更新,保存即生效
  • 本地dev环境无痛切换到production环境,无需修改任何代码
  • 基于webpack-dev-server,和整个项目耦合,无需本地起服务
    本地开发时,配置webpack.dev.config.js,在devServer部分的配置中加入apiMocker即可。
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
const apiMocker = require("webpack-api-mocker");
// ...
devServer: {
// ...
before(app) {
apiMocker(app, path.resolve('./mock/index.js'))
}
}
对应的路径下,写入mock数据和路径即可,可以灵活组织各模块的mock数据:
const proxy = {
'GET /user/info': {
"code": 200,
"message": "success",
"data": {
// your mock data
}
},
'POST /user/update': {
"code": 200,
"message": "success",
"data": null
}
}

module.exports = proxy;

更多使用,参考webpack-api-mocker文档。

webpack配置使用es6语法

如今现代的前端开发早已采用全es6的语法书写,然而webpack的配置文件需要通过node解析执行,一般还使用es5的语法书写。在需要使用importexport,数组、对象解构等最新特性时就很蛋疼。

比如在最近的开发中,使用webpack-api-mocker时,希望拆分不同领域的接口到不同文件,最后通过对象结构的方式聚合在mocker的入口文件中。使用es5的语法就很麻烦。

实际上,让webpack使用babel解析配置文件分两步即可:

  1. yarn add -D babel-register,让webpack能够使用babel-loader转译配置文件
  2. 修改配置文件后缀为,webpack.config.babel.js,webpack会使用.js前的字符串作为loader

之后就可以愉快地使用es6语法写配置文件了。

autoprefixer remove -webkit-box-orient解决方案

autoprefixer是postcss的插件,会根据browser list,删除一些autodated的样式,其中就包括-webkit-box-orient这个用于hack实现多行省略号的CSS样式。

几种方法:

  • 设置autoprefixer,{remove: false},保留autodated的样式规则
  • 添加flexbox 2009老旧浏览器到broswer list中
  • 如下,通过注释临时disable autoprefixer
1
2
/* autoprefixer: ignore next */
-webkit-box-orient: vertical;

读完了《图解HTTP》,就算是对计算机网络和HTTP部分内容做了个温习吧。同步做了整理,以便加强记忆和后面回顾。

概述

请求报文构成:

  • 方法
  • URI(绝对或是相对)
  • HTTP版本
  • 请求首部
  • 内容实体

响应报文构成:

  • HTTP版本号
  • 状态码
  • 状态码原语
  • 响应头部
  • 响应主体

请求URI是服务器本身时,可以用*代替URI。

可用的方法列表:

  • GET 获取资源
  • POST 传输信息
  • PUT 传输文件,没有用户验证机制,很少用到
  • DELETE 删除文件,同上,很少用到
  • HEAD 获得响应头部,不返回主体
  • OPTIONS 询问支持方法
  • CONNECT 用来建立HTTPS连接的隧道
  • TRACE 追踪路径上的所有服务器节点,很少用到

其中后面三个是HTTP1.1才开始支持的。

持久化

在HTTP1.1后,HTTP建立的TCP连接默认是长连接(keep-alive),避免不必要的多次TCP握手和挥手。在此基础上,客户端可以同时向服务端发起多个资源请求。

状态化

HTTP本身是无状态的。通过cookie实现状态化,cookie通过服务端在响应头部的set-cookie字段下发,设置信息、使用范围、过期时间等内容。客户端在使用范围内的请求默认会携带上cookie信息。

HTTP报文结构

  • 请求首部和主体通过CR+LF分割开来
  • 报文编码
    • 编码压缩
      • gzip (GNU zip)
      • compress(UNIX compress)
      • deflate(zlib)
      • indentity(不压缩)
    • 分块发送
    • 多部分发送(multipart)
      • multipart/form-data 表单文件上传,用boundary字符--表示新的part的开始
      • multipart/byterange 配合206响应只包含了部分数据时使用
    • 部分发送
      • Range指定字节范围
      • 206响应状态码
  • 内容协商
    • 双方就合适的语言、字符集、编码方式、过期时间进行协商

HTTP状态码

  • 1xx 这一类型的状态码,代表请求已被接受,需要继续处理
    • 100 Continue:客户端应当继续发送请求。
    • 101 Switching Protocals:将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。
  • 2xx 成功:这一类型的状态码,代表请求已成功被服务器接收、理解、并接受。
    • 200 OK:请求已成功,在方法时HEAD时不返回响应主体
    • 204 No Content:服务器成功处理了请求,但不需要返回任何实体内容,用户浏览器应保留发送了该请求的页面
    • 205 Reset Content:和204的唯一不同是返回此状态码的响应要求请求者重置文档视图
    • 206 Partial Content:服务器已经成功处理了部分GET请求。请求必须包含Range头信息来指示客户端希望得到的内容范围,返回使用Content-Range多用于下载工具
  • 3xx 重定向:这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址在本次响应的Location域中指明。
    • 300 Multiple Choices:被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。
    • 301 Moved Permanently:被请求的资源已永久移动到新位置,建议使用Location中的新地址
    • 302 Found:请求的资源现在临时从不同的URI响应请求
    • 303 See Other:和302的区别是,客户端应当采用GET的方式访问新的资源
    • 304 Not Modified:如果客户端发送了一个带条件(包括缓存相关的请求头部)的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变
    • 305 Use Proxy:被请求的资源必须通过指定的代理才能被访问
  • 4xx 客户端错误:客户端发生了错误
    • 400 Bad Request:由于包含语法错误,当前请求无法被服务器理解
    • 401 Unauthorized:当前请求需要用户验证,或用户未通过验证。
    • 403 Forbidden:服务器已经理解请求,但是拒绝执行它
    • 404 Not Found:资源未被在服务器上发现
    • 405 Method Not Allowed:请求行中指定的请求方法不能被用于请求相应的资源,响应中必须返回一个Allow头信息用以表示出当前资源能够接受的请求方法的列表
    • 406 Not Acceptable:请求的资源的内容特性无法满足请求头中的条件
    • 413 Request Entity Too Large
    • 414 Request-URI Too Long
  • 5xx 服务器错误:服务器在处理请求的过程中有错误发生
    • 500 Internal Server Error:这个问题会在服务器的代码出错时出现
    • 501 Not Implemented:服务器不支持当前请求所需要的某个功能
    • 502 Bad GateWay:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
    • 503 Service Unavailable:临时的服务器维护或者过载。这个状况是临时的,并且将在一段时间以后恢复。
    • 504 Gateway Timeout:作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器或者辅助服务器收到响应

协作机制

  • 代理(Proxy),单纯转发HTTP请求,会在响应头部的Via字段留下痕迹
  • 网关(Gateway),隔绝服务器和客户端,有安全、计费等逻辑
  • 隧道(tunnel),基于协议搭建,保证传输安全,对用户侧透明
  • 缓存(Cache),本地、服务端二级缓存,加快响应时间,有过期时间

报文头部

  • 首部用来进行连接的各种信息描述。每个首部的字段用字段名和值组成,两者用:隔开。
  • 首部分为端到端和逐跳两类,前者在报文转发的整个过程都保留,后者在转发后就会丢弃。典型的逐条首部有Connection, Keep-Alive, Transfer-Encoding, Upgrade

通用首部

Cache-Control

客户端和服务端协商缓存机制。配合下面一些首部字段使用:

  • Etag
  • Last-Modified
  • Expires(HTTP1.0)
  • Pragma(HTTP1.0)
  • Age(HTTP1.0)
  • If-None-Match
  • If-Not-Modified-Since

Cache-Control有下面一些可配置项。

缓冲能力上,

  • private,缓存只针对当前用户而言
  • public,缓存对所有用户生效
  • no-cache,始终对缓存进行过期验证
  • no-store,不允许缓存

过期时间上,

  • min-fresh,返回指定时间范围内的非过期资源
  • max-stale,返回指定时间范围内过期、非过期资源
  • max-age,单位:秒,最大缓存时间
  • s-max-age,同上,只用于CDN缓存

二次验证上,

  • only-if-cached,强制从缓存服务器中获取内容
  • immutable,一旦缓存不可更改
  • must-revalidate,即使本地已缓存,仍要求检查CDN缓存
  • proxy-revalidate,缓存服务器必须检查源内容是否改变

Connection

管理连接,主要有两个用途。

  • 指定不希望转发给代理的字段
  • 管理持久连接。使用Connection: Keep-Alive建立连接(HTTP1.1默认行为),使用Connection: Close终止连接

Date

报文创建时间。行如“Date: Tue, 03 Jul 2012 04:31:12 GMT”

Pragma

历史遗留字段。Pragma: no-cache等同于Cache-Control: no-cache

除此外还有:

  • Trailer,说明报文主体中记录的首部字段
  • Transfer-Encoding,分段传输的主体编码
  • Upgrade,切换协议,配合Connection: Upgrade使用
  • Via,标明沿途的整条路径
  • Warning,缓存相关警告

请求首部

  • Accept 接受文件的类型,类型间可以指定q=x表示权重值,x的取值在0到1之间。下同
  • Accept-Charset 可以接受的文件字符集
  • Accept-Encoding 可以接受的文件编码,有gzip,compress,deflate,indentity几种
  • Accept-Language 可接受的语言
  • Authorization 服务端需要的用户验证信息
  • Age 从缓存实体产生到现在经历的时间
  • Expect 期望的服务端返回状态码,服务端无法满足时返回417状态码,客户端等待服务端100响应时发送的请求都要带上该字段
  • Host 服务器的主机名,通常是请求资源的URL
  • If-Match 需要匹配的Etag,不满足时返回412,表示不满足条件
  • If-Modified-Since 返回指定日期后的新内容,否则返回304
  • If-Unmodified-Since 类似上
  • If-Range 类似If-Match不过是范围匹配
  • Max-Forwards 报文最多转发次数,通常配合TRACE方法使用
  • Proxy-Authorization 代理服务端需要的用户验证信息
  • Range 请求资源的部分内容,一般用在多线程下载(客户端发起)
  • Referer 当前请求从哪个地址发起
  • User-Agent 请求发起终端信息

响应首部

  • Accept-Ranges,表示服务器是否支持Range请求,支持时值为bytes,否则是none
  • Age,表示缓存到目前为止过了多久(HTTP1.0)
  • Etag,资源的唯一标识,分为强Etag和弱Etag
  • Location,用在3xx的请求中,表示客户端需要重定向到的新地址
  • WWW-Authentication/Proxy-Authentication,服务器、代理使用的认证类型
  • Server,服务器信息
  • Vary,与Vary指定首部字段同名的请求才会命中缓存

实体首部

  • Allow 允许的访问方法
  • Content-Encoding/Content-Language/Content-Length/ 内容的编码、语言、长度、类型
  • Content-Location 内容的位置,通常在和访问URI时会用到
  • Content-MD5 内容MD5编码,便于和客户端编码后进行对比,防止内容篡改
  • Content-Range 用于部分请求
  • Content-Type 文件类型,包括MIME type和字符集
  • Expires/Last-Modified 文件的过期时间和上次修改时间,用户判断缓存是否过期

除此之外,还有和Cookie相关的两个头部,它们来自网景公司对于Cookie的设计。

  • Set-Cookie,服务端下发设置Cookie信息。包含下列信息
    • expires,过期时间
    • path,适用路径
    • domain,适用域名
    • secure,限制https才会携带Cookie
    • HttpOnly,限制JS脚本访问Cookie
    • 下发的cookie内容
  • Cookie,客户端期望的cookie内容

另外还有一些常用的首部字段:

  • X-Frame-Options,规定页面在iframe中的呈现方式
    • DENY 禁止访问
    • SAMEORIGIN 仅允许同源访问
  • X-XSS-Protection,为1时开启XSS防御

不建议使用”X-“开头的方式拓展非标准首部

HTTPS简介

HTTP缺点:

  • 使用明文 -> 通信内容可以被窃听 –HTTPS–> 加密通信内容
  • 不能验证身份 -> DDoS攻击和伪装服务器、客户端身份 –HTTPS–> 证书证明身份
  • 不能验证内容完整性 -> 中间人攻击 –HTTPS–> HTTPS保证完整性

HTTPS特征:

  • 加密内容
  • 证书
  • 完整性保护

HTTPS建立在SSL连接之上,SSL建立在TCP连接上。SSL使用共享秘钥和公开秘钥加密两种方式混合加密。在秘钥确保安全的情况下,使用共享秘钥对称加密,优化速度;否则使用公开秘钥确保安全性。

  • 共享秘钥,双方使用同一秘钥加密和解密,秘钥被监听后加密就失去了意义
  • 公开秘钥,使用公开秘钥加密,使用私有秘钥解密

然而公开密钥本身并不能确保完整性,需要证书机构(CA)颁发证书认证,确保秘钥和端的有效以及合法性。服务端也可以使用OpenSSL为自己颁发自认证证书,但是一般会在浏览器上弹出警告。

HTTPS建立连接的过程包括:

  1. 协商决定秘钥组件
  2. 服务端发送公开密钥、证书
  3. 客户端检查证书合法性,以确认服务端身份,并拿到公钥
  4. 客户端发送pre-master secret随机字符串
  5. 服务端使用私钥加密pre-master secret hash值,返回加密的hash值(避免黑客尝试破解私钥)
  6. 客户端使用公钥解密hash,对比自己之前生成的pre-master secret字符串hash,若一致,及证明服务端身份的合法性
  7. 客户端生成一个对称加密算法和秘钥master-secret,使用公钥加密,发送给服务端
  8. 双方使用master-secret进行通信

通信的完整性可以通过将报文内容生成hash交由客户端验证来实现。

SSL最初由网景开发,1.0和2.0版本被发现存在问题已被废弃。3.0后由IETF接手。目前可用的协议版本有SSL3.0和TLS1.0、TLS1.1、TLS1.2,其中最常用的是SSL3.0和TLS1.0。

证书

证书包含:

  • 发布机构(CA)
  • 有效期
  • 持有者(由CA担保证明持有者身份)
  • 公钥
  • 数字签名算法
  • 指纹算法

为了保证安全,在证书的发布机构发布证书时,证书的指纹和指纹算法,都会用自己的私钥加密后再和证书放到一起发布。使用者在打开证书时,根据加密算法,系统使用自带的公钥解密指纹和指纹算法,使用指纹算法计算证书的hash值和指纹对比,如果对的上就代表证书没问题。系统使用的公钥和证书一般由证书发布机构自己生成,内嵌在操作系统中。

证书颁发机构(CA)通常会去做很多工作确保持有者的合法性,信任CA代表着信任CA颁发证书中的所有信息。所以一般系统只选择信誉较好的CA机构。公司内部使用或自生成的证书就只能被在指定范围内被信任。

身份验证

  • BASIC 使用用户名密码验证,明文传输
  • DIGEST 质询响应,防止密码被拦截,安全度和便利性都较差
  • SSL 客户端证书 + HTTPS传输,成本高
  • HTTP表单 + Cookie/Session验证

功能追加协议

WebSocket

全双工,解决Ajax,长短轮询的局限。握手过程很简单:

  • 请求方添加Upgrade首部字段,声明升级到websocket。包含Sec-WebSocket-Key,Sec-WebSocket-Protocol,Sec-WebSocket-Verison等必要字段
  • 响应方回复101状态码,包含Sec-WebSocket-Accept(是根据Sec-WebSocket-Key生成的),Sec-WebSocket-Protocol

连接建立后,双方使用WebSocket的方式进行通信

WebDAV

基于Web的文件属性管理。新增了一些方法和状态码,允许客户端远程修改服务器上的文件。

Web应用

RSS

RDF Site Summary,简易内容聚合。和Atom一样,使用XML的形式发布信息,通过特定的RSS阅读器阅读。

常见Web攻击方式

根本原因:HTTP本身没有必要的安全机制。

输出值转义相关攻击方式

  • XSS,跨站脚本攻击,主要出现在动态拼接HTML的场景中,用户恶意注入的script代码段埋下陷阱,诱导用户误操作触发。盗取用户密码或Cookie信息
  • SQL注入,通过URL注入的方式,制造恶意SQL语句,出现在动态拼接SQL语句的场景下。可以绕过认证、甚至破坏整个数据库
  • OS命令注入,类似SQL注入,出现在动态拼接OS语句的场景下。
  • HTTP首部攻击,出现在服务端响应头部使用了用户侧输入场景下,比如302响应中的Location头部可能存在的query部分。攻击者可以通过添加换行符,恶意添加新的首部字段,甚至篡改原有的响应主体
  • 邮箱首部注入攻击,类似HTTP首部攻击

类似地还有目录遍历漏洞、远程文件引用漏洞。

通过上面几种攻击方式,可以看到,永远不要信任用户侧输入使用白名单机制,禁止动态拼接用户输入的语句

设计缺陷相关攻击方式

  • 强制浏览,在服务器公开目录下,浏览开发者本非自愿公开的文件。
  • 不正确的系统错误处理方式,数据库等内部系统抛出的错误,对用户毫无帮助,反倒能让攻击者看到服务背后的一些细节。包括,数据库错误、PHP等脚本错误、Web服务器的错误
  • 开放重定向,网站有诸如?redirect=xxx的path可以重定向时,一定要对redirect后的网址进行白名单控制,防止成为钓鱼攻击的跳板

session相关

  • XSS盗取cookie,伪装用户登录
  • 发送恶意链接,强制用户使用攻击者指定的session ID
  • CSRF,跨站信息伪造,在带有用户信息的domain里留下恶意的网络请求,伪造用户发起请求,伪造请求可以通过<img src="xx" />, <video src="xxx></video>等多种形式

其他

  • 穷举法破解密码,暴力破解。使用图片验证码、手机验证码、机器检测等方式限制同IP的访问频率。
    • 彩虹表。使用salt,增加破解难度
  • 撞库。建议用户在不同域内使用不一样的密码
  • 点击劫持,使用透明元素覆盖在目标网页上。在18+网页中最常出现(😂)。
  • DoS(Denial of Service)拒绝服务攻击,构造大量合法的网络请求,导致服务器超负荷。通常都是DDoS(Distributed Denial of Service)的形式。需要在IP层去过滤攻击的IP。
  • 后门程序

原版链接: https://book.douban.com/subject/25820714/

准则

  • 减少用户思考
  • 减少用户心理负担

原因: 用户时间有限,界面必须易于理解

用户的使用方法

  • 82原则,只扫描感兴趣的
  • 用户只寻求一个可行而非最优的答案
  • 用户并不关心产品如何运作,会按照某个可用的方式一直使用下去

方法论

  • 利用习惯性思维,包括页面位置、使用方法、元素外观这些被培养起来的习惯。如无必要,勿增实体。
  • 层次分明,逻辑上的关联能从视觉上直接体现;能够划分区域
  • 明确标识可交互元素;提高信噪比,减少不必要视觉干扰
  • 标题更靠近关联的内容,突出关键词汇
  • 减少冗余文本,包括欢迎语、指示文字

Web导航

重要性:

  • 用户在web中感受不到方位
  • 用户需要更快地达成目标

习惯用法:

  • 导航部分(或是某些公用部分)固定出现在页面同样位置,会让用户能立即确认自己还处在这个网站里
    • 市场类应用里,包括站点ID、栏目、实用工具、搜索
  • 站点ID需要有独特可区分的设计
  • 一个返回首页的导航链接
  • 简洁明了的搜索框
    • 简单的按钮文案
    • 减少无用的提示文字
    • 明确可能的搜索选项(如果有的话)
  • 每个页面需要有个名字(保留意见)
    • 合适位置
    • 引人注目
    • 和链接保持尽量一致
  • 明确告诉用户“我在哪儿”
    • 面包屑
    • tab
  • 上述元素主要的原因:现实生活中,用户并不会按照设计师规划好的路径访问网页,可能会来自分享链接、搜索引擎,并不能保证从入口进入。要能让用户在任意一个页面都可以清楚明白它要完成某项任务的话,应该如何使用当前这个网页。

吸引用户时需要注意的地方

主页要能传达整体印象。必须能显而易见地直截了当地明白:

  • 这是什么网站
  • 我能在里面做什么
  • 网站里有什么
  • 为什么选择这个网站

在用户弄清楚这些问题的最初几秒甚至是最初几毫秒,是决定你能否留住他的关键(晕轮效应)。而且因为上面加粗字体的原因,你可能要在主页外的其他页面也保证这一点。

一些手段:

  • 靠近站点ID的简洁的slogan
  • 一些推介语
  • 以明确主张为目标占用空间
  • 在描述使命时保证坦诚
  • ab test和数据说话

好口号和好的站点ID一样,是非常重要的。它需要至少有下面几点特征:

  • 清晰简洁、言之有物
  • 明确产品特色与优势,最好是只有你能适用,别的产品都用不了的那种
  • 最好能再个性、俏皮一点

当然你们公司足够出名的话,上面这些就当不存在就行。

接下来的任务就是,告诉用户该从哪里开始和避免滥用首页推介。

怎样减少信仰讨论

  • 避免关于个人喜好的讨论(如:“我不喜欢下拉框”)
  • 针对场景,根据经验选择(如:“我认为这种场景下不适合下拉框”)
  • 充分测试,数据说话反哺经验

如何进行可用性测试

当下的互联网公司迭代速度之快,可能并没有时间做这方面的研究。

区分开焦点测试(类似于种子用户,听取他们的使用感受和反馈)和可用性测试。

  • 焦点测试在早期阶段
  • 可用性测试持续进行
  • 周期性(比如一个月)进行可行性测试
  • 暴露严重问题,因为团队可能并没有资源解决所有问题
  • 应该有个主持人

最有可能的测试流程:

  • 介绍部分
  • 简单的提问部分,了解测试者的背景
  • 简单的主页浏览,询问感受
  • 完成测试任务
  • 问题询问

典型的问题:

  • 用户不清楚概念
  • 用户找不到想要的字眼
  • 内容太多了

移动时代带来的挑战

  • 狭小空间的约束
    • 用户需要立即完成或经常重复的工作应该一样就能看到
    • 其他的事情应该轻点几下就能完成,而且有显而易见的路径到达
  • 兼容多平台的UI解决银弹是很难的
  • 在UI上给用户足够的按钮,比如一个有着三维样式闪着光的按钮
  • 意识到没有光标了
  • 应用最好能“让人快乐”
  • 移动应用尤其需要可学习,然后是可记忆

用户的好感度

降低好感度的几种方式

  • 隐藏用户想要的信息
  • 对用户交互不宽容
  • 询问用户过多信息
  • 敷衍用户
  • 看上去不专业

如何提升好感度

  • 知道用户想要什么
  • 简明易懂
  • 看上去花了心思
  • 知道用户的可能问题,并给予解释
  • 提高鲁棒性

如何说服你的老板推进可用性

参考:my-git/git-workflow-tutorial.md at master · xirong/my-git

git工作流有多种使用方法,在实际工作中的不良工作习惯,会造成很让人头大的麻烦。下面距离一些常用的工作流。

集中式

类似SVN,集中式工作流以中央仓库作为项目所有修改的单点实体,只用到master这一个分支。开发者提交功能修改到中央库前,采用rebase的方式“在其基础上添加自己的修改”,得到完美的线性历史;遇到冲突时,通过git statusgit add合并冲突。最后git rebase --continue即可。遇到困难无法进行下去时,git rebase --abort就可以撤回到rebase前的状态。

在这种工作流下,使用rebase参数比不使用的git pull好处在于,rebase后的提交记录会少一次累赘的“合并提交”。

功能分支

git相较SVN强大在分布式的特征。功能分支工作流主要针对新增功能集成到正式项目。功能分支工作流仍然以中央仓库为基础,但不是直接提交本地历史到各自的本地master分支,而是在开发新功能时创建新的分支,描述新功能。不同的功能分支相互隔离,同时也保证master分支的代码一定没有问题。一旦功能分支push到master,意味着功能与其他开发者共享。

合并到master分支的过程通过创建pull request进行,在pull request请求中,让其他开发者有机会先去review变更。Pull request被接受后,剩下的工作就和集中式很像了,拉取master分支代码,合并,提交。

工作流程上:

  1. 先checkout功能分支
  2. 做本地开发提交,以及push -u推送到远端分支(-u是跟踪远端对应分支的意思)
  3. 完成开发后,提交pull request,请求合并远端功能分支到master,团队其他成员可以进行评论
  4. 在接受前,团队所有成员有需要,可以提交自己的修改到该功能分支,也会显示在pull request里
  5. 在pull-request被接受后,在本地master上可以用pull或者pull -r的方式合并功能分支,前者更像功能和原来代码的合并,后者更偏向线型的提交历史

gitflow

Gitflow工作流通过为功能开发、发布准备和维护分配独立的分支,让发布迭代过程更流畅。相较功能分支更复杂,但也更健壮。仍然用中央仓库作为所有开发者的交互中心。相对于使用仅有的一个master分支,Gitflow工作流使用两个分支来记录项目的历史。master分支存储了正式发布的历史,而develop分支作为功能的集成分支。从而可以在master的所有提交附上版本号

每个新功能位于一个自己的分支,有着和功能分支一样的开发工作流,唯一不同的是,功能分支不是从master分支上拉出新分支,而是使用develop分支作为父分支。每次合并都位于develop分支。

一旦develop分支上有了做一次发布(或者说快到了既定的发布日)的足够功能,就从develop分支上checkout一个发布分支release。从这个时间点开始之后新的功能不能再加到这个分支上——这个分支只应该做Bug修复、文档生成和其它面向发布任务。在release工作完成后,合并release分支到master,并加上tag。同时,release上做的修改要合并会develop分支。最后删除release分支。

维护分支或说是热修复(hotfix)分支用于给产品发布版本(production releases)快速生成补丁,这是唯一可以直接从master分支fork出来的分支。修改完成后,修改应该立马合并回master和develop。master也应该为合并生成新的tag。

forking

Forking工作流是分布式工作流,可以安全可靠地管理大团队的开发者(developer)和不信任贡献者(contributor)的提交。这种工作流不是使用单个服务端仓库作为『中央』代码基线,而让各个开发者都有一个服务端仓库。这意味着各个代码贡献者有2个Git仓库而不是1个:一个本地私有的,另一个服务端公开的。Forking工作流的一个主要优势是,贡献的代码可以被集成,而不需要所有人都能push代码到仅有的中央仓库中。开发者push到自己的服务端仓库,而只有项目维护者才能push到正式仓库。

新开发者想要在项目上工作时,不是直接从正式仓库克隆,而是fork正式项目在服务器上创建一个拷贝。这个仓库拷贝作为他个人公开仓库 —— 其它开发者不允许push到这个仓库,但可以pull下来修改。要提交本地修改时,push提交到自己公开仓库中 —— 而不是正式仓库中。 然后,给正式仓库发起一个pull request,让项目维护者知道有更新已经准备好可以集成了。为了集成功能到正式代码库,维护者pull贡献者的变更到自己的本地仓库中,检查变更以确保不会让项目出错, 合并变更到自己本地的master分支, 然后push master分支到服务器的正式仓库中。到此,贡献的提交成为了项目的一部分,其它的开发者应该执行pull操作与正式仓库同步自己本地仓库。

具体来说,大致有下面几步:

  1. 开发者fork正式仓库
  2. 开发者clone自己的fork出来的仓库,与之前工作流不一样的是,Forking工作流需要2个远程别名 —— 一个指向正式仓库,另一个指向开发者自己的服务端仓库。,像下面这样
    1
    git remote add upstream https://bitbucket.org/maintainer/repo
  3. 开发者修改都是私有的,如果项目往前走了,可以用git pull获得新的提交
  4. 开发者准备分享新功能时,需要先push到自己的公开仓库中,然后发起pull request通知项目维护者,集成开发者的功能分支
  5. 项目维护者通过GUI岔开pull request或者pull代码到自己的本地仓库,再手动合并。
  6. 开发者通过pull upstream master的方式拉取项目最新进展

pull request

pull request用于合并不同分支或不同仓库的代码,并在合并前进行一些讨论和代码微调,在上面不同工作流的情况下具体功能体现也不同。

上面几种工作流范式只是几种标准的建议,正式的项目版本管理中,可以糅合上面的一些特点。

0%