编码和加密

本文最后更新于:2021年4月29日 上午

编码

Ascii

ASCII (American Standard Code for Information Interchange)美国信息交换标准代码 是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言
它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646
ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符

码表

在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示10

例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)

在计算机中存储时也要使用二进制数来表示,为了记录具体用哪些二进制数字表示哪个符号,人们建立了码表。每个人都可以约定自己的一套编码方式(码表)
为了让大家互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示

相互转换

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
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : 0.py
@Time : 2021年1月11日
@Author : Recluse Xu
@Version : 1.1
@Contact : 444640050@qq.com
@Desc : ASCII 相关
'''


def char_to_ascii_num(text: str):
for char in text:
print(char, ord(char))

def ascii_num_to_char(char_num_list : str):
for char_num in char_num_list:
print(char_num, chr(char_num))

if __name__ == '__main__':
char_to_ascii_num('Hello world')
print('----------------')
ascii_num_to_char([72, 101, 108, 108, 111, 32, 119, 111, 114, 114, 108, 100])

Base64

Base64是一种基于64个可打印字符来表示二进制数据的方法
是网络上最常见的用于传输编码方式之一
3个字节 = 8位 = 24比特, 对应于4个Base64单元
即3个字节可以由4个可打印字符来表示

可打印字符:A-Z、a-z、0-9 (一共62个字符)
剩余两个字符在不同的系统中表示不同

多用于处理文本数据 与 二进制数据的表示、传输、存储
例如:网页上的图片

例: 输入6666

由于后面还没够3字节,所以就会补充一些字符

Base64核心原理是将二进制数据进行分组,每 24Bit/3字节 一大组,再把大组的数据分成 6Bit 的小分组

由于6Bit数据只能表示64个不同字符(2^6=64),所以叫Base64

大厂会自己定制特定的字符表,来达到混淆的目的
虽然都是base64, 但其个字符与值的关系是被更改过的

码表


默认的码表是 大写字母 + 小写字母 + 数字 + '+' + '/'
这东西其实能自己定义

浏览运行过程

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : 7_base64_charset.py
@Time : 2020/08/27 12:05:58
@Author : Recluse Xu
@Version : 1.0
@Contact : 444640050@qq.com
@Desc : 自定义字符集的base64编码与解码
'''

# here put the import lib
import string
import random

'''
定义自己的64个字符
'''

# 从标准库里将大写字母,小写字母,数字(一共62个字符)
char64 = string.ascii_uppercase + string.ascii_lowercase + string.digits
# 额外添加两个字符(一共64个字符)
char64 += '+'+'/'
char64 = list(char64)
# 打乱码表
random.shuffle(char64)


def cut(obj, sec):
'''
将字符串按照指定数量进行切分
'''
return [obj[i:i+sec] for i in range(0, len(obj), sec)]


def ascii_2_base64(ascii_str: str) -> str:
'''
正常来说,一个ASCII字符占8位
一个base64字符占6位
3*8 = 24 = 4*6位
这个函数就是用来做这个转换的
单个字符位数不够的,计算时会用0补全
整个字符位数不够的,在最后会用=来补足

b1 b2 b3
n1 n2 n3 n4

'''
print('传入的数据字符串\t', ascii_str)
# 将数据转为bytes
origin_bytes = ascii_str.encode()
print('将传入数据转为bytes\t', origin_bytes)

# 转为八位二进制
base64_8_bin = [f"{str(bin(b)).replace('0b', ''):0>8}" for b in origin_bytes]
print('将数据转为八位二进制\t', base64_8_bin)

# 按每6位切分
base64_6_bin = cut(''.join(base64_8_bin), 6)
print('每6位切分一次数据\t', base64_6_bin)

# 最后一位位数补足到6位
base64_6_bin[-1] += '0' * (6 - len(base64_6_bin[-1]))
print('将最后一位的位数补足\t', base64_6_bin)

# 将被切分的数据重新转为10进制
base64_int = list(map(lambda x: int(x, 2), base64_6_bin))
print('每段二进制数转十进制\t', base64_int)

# 码表中寻找目标字符替换对应项
base64_str = list(map(lambda x: char64[x], base64_int))
print('码表中寻找目标字符替换\t', base64_str)

# 位数不够的地方补=
base64_str += ['='] * (3 - len(base64_8_bin) % 3)
print('用=将数据补全到24*n位\t', base64_str)

# 最终结果
base64_str = ''.join(base64_str)
print('ascii转base64 最终结果\t', base64_str)
return base64_str


def base64_2_ascii(base64_str: str) -> str:
'''
做和上面相反的操作
'''
print('传入的数据字符串\t', base64_str)
# 处理最后一个字符,通过码表判断是否为填充用的字符,是则在记录后去除
if base64_str[-1] not in char64:
base64_str = base64_str.replace(base64_str[-1], '')
print('按码表处理填充字符\t', base64_str)

# 码表中寻找目标十进制数字序号替换对应项
base64_int = list(map(lambda x: char64.index(x), base64_str))
print('码表中寻找目标序号替换\t', base64_int)

# 十进制数字序号转六位二进制
base64_6_bin = [f"{bin(x)[2:]:0>6}" for x in base64_int]
print('十进制序号转六位二进制\t', base64_6_bin)

# 六位二进制转八位二进制
base64_8_bin = cut(''.join(base64_6_bin), 8)
print('六位二进制转八位二进制\t', base64_8_bin)

# 抛弃位数不足的位数
base64_8_bin = list(filter(lambda x: len(x) == 8, base64_8_bin))
print('抛弃位数不足八位的项\t', base64_8_bin)

# 八位二进制转ascii字符
ascii_str = [chr(int(x, 2)) for x in base64_8_bin]
print('八位二进制转ascii字符\t', ascii_str)

ascii_str = ''.join(ascii_str)
print('base64转ascii 最终结果\t', ascii_str)


if __name__ == '__main__':
base64_str = ascii_2_base64('Hello')
print()
base64_2_ascii(base64_str)

Unicode

Unicode,中文又称万国码、国际码、统一码、单一码,是计算机科学领域的业界标准
它整理、编码了世界上大部分的文字系统,使得电脑可以用更为简单的方式来呈现和处理文字

Unicode 为每一个字符而非字形定义唯一的代码(即一个整数)
换句话说,统一码以一种抽象的方式(即数字)来处理字符,并将视觉上的演绎工作(例如字体大小、外观形状、字体形态、文体等)留给其他软件来处理,例如网页浏览器或是文字处理器

设计原则

  • Universality:提供单一、综合的字符集,编码一切现代与大部分历史文献的字符
  • Efficiency:易于处理与分析
  • Characters, not glyphs:字符,而不是字形
  • Semantics:字符要有良好定义的语
  • Plain text:仅限于文本字符
  • Logical order:默认内存表示是其逻辑序
  • Unification:把不同语言的同一书写系统(scripts)中相同字符统一起来
  • Dynamic composition:附加符号可以动态组合
  • Stability:已分配的字符与语义不再改变
  • Convertibility:Unicode与其他著名字符集可以精确转换

文种平面

Unicode字符分为17组编排,每组称为平面(Plane),而每平面拥有65536(即216)个代码点
然而目前只用了少数平面

平面 始末字符值 中文名称 英文名称
0号平面 U+0000 - U+FFFF 基本多文种平面 Basic Multilingual Plane,简称BMP
1号平面 U+10000 - U+1FFFF 多文种补充平面 Supplementary Multilingual Plane,简称SMP
2号平面 U+20000 - U+2FFFF 表意文字补充平面 Supplementary Ideographic Plane,简称SIP
3号平面 U+30000 - U+3FFFF 表意文字第三平面 Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面 U+40000 - U+DFFFF (尚未使用)
14号平面 U+E0000 - U+EFFFF 特别用途补充平面 Supplementary Special-purpose Plane,简称SSP
15号平面 U+F0000 - U+FFFFF 保留作为私人使用区(A区) Private Use Area-A,简称PUA-A
16号平面 U+100000 - U+10FFFF 保留作为私人使用区(B区) Private Use Area-B,简称PUA-B

实际,光是 BMP基本多文种平面 就已经非常够用了,现在绝大多数的常用语言都能在这里找到(包括中文)

维基百科:Unicode字符平面映射

编码方式

目前实际应用的统一码版本对应于UCS-2,即使用 16位 的编码空间。也就是每个字符占用2个字节

I 0049 00000000 01001001
77e5 01110111 11100101

这样理论上一共最多可以表示 2^16 = 65536 个字符
基本满足各种语言的使用
实际上当前版本的统一码并未完全使用这16位编码,而是保留了大量空间以作为特殊使用或将来扩展

实现方式

Unicode的实现方式不同于编码方式
一个字符的Unicode编码确定。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同

Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF

I 0049 00000000 01001001
77e5 01110111 11100101
严格按照UCS-2会浪费空间,因为英文只需要一个字节就能够表示出来
那一长串的 0 没有记录与传输的必要,因此有了不同的实现方式(比如UTF-8)

Unicode转换样例

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
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : 8_unicode.py
@Time : 2020-8-29 00:21:05
@Author : Recluse Xu
@Version : 1.0
@Contact : 444640050@qq.com
@Desc : 基础unicode操作
'''


def char_2_unicode_char(char: str) -> str:
# 字符编码为unicode字符
c = char.encode('unicode_escape')
c = str(c)
print(char, 'Unicode编码结果', c)
return c


def unicode_char_2_char(unicode_char: str) -> str:
# unicode字符解码为字符
c = unicode_char.encode().decode('unicode_escape')
print(unicode_char, 'Unicode编码结果', c)
return c


def char_2_unicode_ret(a_char: str) -> int:
# 得到一个字符的unicode 数值
c = ord(a_char)
print(a_char, '对应Unicode值', c)
return c


def unicode_ret_2_char(a_ret: int) -> int:
# 根据unicode 数值,得到一个字符
c = chr(a_ret)
print(a_ret, '对应字符', c)
return c


def unicode_ret_2_unicode_char(a_ret: int):
# 根据unicode 数值,得到一个unicode字符
c = chr(a_ret).encode('unicode_escape')
print(a_ret, '对应字符', c)
return c


if __name__ == "__main__":
c = '齤'
char_2_unicode_char(c)
unicode_char_2_char('\\u4f60\\u597d')

char_2_unicode_ret(c[0])
unicode_ret_2_char(38006)
unicode_ret_2_unicode_char(38006)

其它

Unicode 定义了一些奇怪的字符在里面,比如:
http://www.unicode.org/emoji/charts/full-emoji-list.html#1f600

UTF-8

UTF-88-bit Unicode Transformation Format)是一种针对 Unicode 的可变长度字符编码,也是一种前缀码

其设计的主要目的是在能正常使用所有字符的同时,减少常用字符(主要指英文/拉丁文)编码长度

编码规则

  1. 单字节的字符,字节的第一位设为0,对于英语文本,UTF-8码只占用一个字节,和ASCII码完全相同;
  2. n个字节的字符(n>1):
    第一个字节的前n位设为1
    第n+1位设为0,后面字节的前两位都设为10,这n个字节的其余空位填充该字符unicode码,高位用0补足

I = 0049 = 00000000 0100100101001001
= 77e5 = 01110111 1110010111100111 10011111 10100101

你会发现,UTF-8 编码对于 Unicode字符码 本身较短的字符较为友好,而对于较后的则并不友好(甚至一顿操作后还变长了)

维基百科:UTF-8

百分号编码-URL编码

百分号编码(英语:Percent-encoding),又称:URL编码(URL encoding)是特定上下文的 统一资源定位符URL 的编码机制,实际上也适用于 统一资源标志符URI 的编码

字符类型

  • URI允许字符
    • 保留字符
      保留字符存在特殊含义
    • 未保留字符
      未保留没有特殊含义
  • URI不允许字符

保留字符与未保留字符

保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ]
未保留字符: 大小写字母,09数字,- _ . ``

URI中的其它字符必须用百分号编码
如果希望使用保留字符,那么必须要经过百分号编码

|!|#|$|&|'|(|)|*|+|,|/|:|;|=|?|@|[|]|
|–|–|
|%21|%23|%24|%26|%27|%28|%29|%2A|%2B|%2C|%2F|%3A|%3B|%3D|%3F|%40|%5B|%5D|

其它字符
建议先转换为UTF-8字节序列, 然后对其字节值使用百分号编码
二进制数据
应该表示为8位一组的序列,然后对每个8位组按照上述方式百分号编码. 例如,字节值0F (十六进制)应表示为%0F

MD5

信息指纹
MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value)
多用于确保信息传输完整一致

特点

输入任意长度的信息,经过处理,都会输出128位的信息(信息指纹)

获取

1
2
3
4
5
6
7
def get_string_md5(text):
m = hashlib. md5()
m. update(text.encode("utf-8"))
print(m. hexdigest())

if __name__ == '__main__':
get_string_md5('Hello world')

更多信息

MD5是不可逆的,是不能算回原文的
网上所谓的破解只是弄了一个超级大的数据,把绝大多数常用的东西算出来MD5结果,你输入什么就返回什么

MD5的抗碰撞性已经被人破解
简单而言,就是能根据一个MD5值,通过一些算法,快速得得到一些内容,其MD5结果与原本MD5一致
校验码一致,内容不一致,使得校验的安全性无法确定

加密

加密算法类别

对称加密算法

一方通过秘钥将信息加密后,将密文传给另一方,另一方通过相同的秘钥解开密文得到明文

非对称加密算法

A要向B发送消息,A与B都要昌盛一对用于加密和解密的公钥和私钥,公钥用于加密,私钥用于解密
各自的私钥各自保密,各自的公钥对对方公开

  1. A要发送信息时,A用B的公钥加密信息
  2. A将密文用B公钥加密过后的信息发送给B
  3. B收到密文后,用B自己的私钥解密密文

对于B发送给A的信息也一样
由于只有私钥能够解密,这个过程中所有其它收到密文和公钥的人都无法解密信息

区别与实际

对称加密算法 非对称加密算法
安全性 较低 较快
处理速度 较快 较慢

常见的情况是:将 对称加密的秘钥 用 非对称加密公钥 进行加密,接收方使用 非对称加密私钥 解密得到 对称加密的秘钥,然后双方使用 对称加密 进行通信

AES

AES高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准
AES是对称秘钥加密中最流行的算法之一
AES是DES的替代品

要素

秘钥

支持三种秘钥长度128/192/256
秘钥越长,越安全,但是处理速度也越慢

填充 Padding

AES并非一股脑将明文加密成密文的,而是把明文拆分成一个个独立的明文块(一块128bit)加密的
如果一段明文拆开多个块后,最后一个块没到128bit,那么久需要对明文块进行填充

常见填充类型
  • NoPadding
    要求明文本身就符合分块要求,不允许不符合要求的明文
  • ZeroPadding
    用0进行填充,填充到位数够为止
    并不推荐使用,当文明快最后一位是0时,解密可能出错
  • PKCS7Padding
    推荐使用
    假设数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小

工作模式

AES的工作模式,体现在把明文块加密成密文块的处理过程中
AES加密算法提供了五种不同的工作模式CBC、ECB、CTR、CFB、OFB
模式之间的主体思想是相似的,在处理细节上有区别

ECB模式

最简单的模式,此模式下,每个明文块的加密都是独立的,互不干涉的
优点是简单快捷,有利于并行计算
缺点是明文相同的块会变成相同的密文块,安全性比较差

CBC模式

CBC模式引入了一个新的概念:初始向量IV

初始向量IV
其作用与 MD5加盐 类似,目的是为了防止 同样的明文块 被加密成 同样的密文块

CBC模式在每一个明文块加密前会让明文块和一个值做异或操作
IV作为初始化变量,参与第一个明文块的异或,后续的每一个明文块和它前一个明文块所加密出来的密文快相异或
最终得到 同样的明文块 被加密成 不同样的密文块
优点:安全性被提高
缺点:无法并行计算,性能不比ECB。引入了IV增加了复杂度

流程

  1. 把明文按128bit拆分为多个明文块
  2. 按照选择的填充方式填充最后一个明文块
  3. 每一个明文块利用AES加密器和秘钥加密成密文块
  4. 拼接所有的密文块,得到结果

示例

简单的过程表示

secret = encrypt(key_type, message)
输入 加密信息,明文,得到加密结果
message = decrypt(key_type, message)
输入 加密信息,密文,得到明文信息

常用的对称加密算法

算法 秘钥长度 工作模式 填充模式
DES 56/64 ECB/CBC/PCBC/CTR/… NoPadding/PKCS5Padding/…
AES 128/192/256 ECB/CBC/PCBC/CTR/… NoPadding/PKCS5Padding/PKCS7Padding/…

秘钥的长度越长,加密越安全,但处理速度越慢
工作模式与填充模式可以看做是对称加密算法的参数和格式选择

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
import base64
from Crypto.Cipher import AES
class AES_Cipher(object):
def __init__(self, key):
self.bs = 16
key = key.encode("utf-8")
self.cipher = AES.new(key, AES.MODE_ECB)

def encrypt(self, raw):
raw = self._pad(raw)
raw = raw.encode("utf-8")
encrypted = self.cipher.encrypt(raw)
encoded = base64.b64encode(encrypted)
return str(encoded, 'utf-8')

def decrypt(self, raw):
decoded = base64.b64decode(raw)
decrypted = self.cipher.decrypt(decoded)
return str(self._unpad(decrypted), 'utf-8')

def _pad(self, s):
# 填充算法,由于算法需要特定位数,位数不足就需要填充
return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)

def _unpad(self, s):
return s[:-ord(s[len(s)-1:])]

if __name__ == "__main__":
a = 'HelloWorld'
aes = AES_Cipher('abcdefgh12345678')
w = aes.encrypt(a)
print(w)
print(aes.decrypt(w))

编码与加密常识

基础通识

  • MD5
    提取结果通常是 32 位,不受明文长度影响
  • Base64
    编码结果末尾通常会出现一或二个等于符号,受明文长度影响
  • SHA1
    加密结果值为 40 位,不受明文长度影响
  • SHA256
    加密结果值为 64 位,不受明文长度影响

盲猜技巧

  • 一长串无规律数字与字母组合的字符大概率是 AES、DES、SHA 相关加密
  • 另外,AES、RSA 等对称和非对称加密都喜欢将结果值用 Base64 进行编码,这样易于传递
  • 如果你看到一长串字符里出现 +\ 和末尾的 = ,那大概率就是上一行描述的加密算法加密后又进行了 Base64 编码的结果
  • 32 位的字符串有概率是 MD5 摘要结果
  • 64 位的字符串有概率是 SHA 加密结果

通过全局搜索找寻可疑字符串,在所有文件中寻找关键字
在需要的地方打上断点,找寻到目标的加密逻辑

复现做法

在找寻到以后,可以选择:

  • 参照js逻辑用其它语言重新实现
  • 直接调用js的加密函数

从非常长的 javascript文件 中归纳逻辑,建议将一些关键的变量或者函数复制粘贴到另一个 javascript文件 中,运行以后根据 没有定义 的报错 来逐一补全所需,直到得到结果


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!