TLHorse 发表于 2021-1-22 09:41

使用零宽字符对文本加密的实现

> 本文为 www.52pojie.cn 首发
> 《使用零宽字符对文本加密的实现》
> @TLHorse

# 0x0 前言
说来话长。其实就是前几天我看到了一篇介绍Unicode的文章,里面介绍Unicode字符的广泛性。其中有一类字符叫做零宽字符,它们在电脑上输入,不可见,也不可打印,甚至输入都不会占空间,作用是控制文字排列或解决个别语言中的排版问题。

常见的零宽字符有以下六种:

| 中文名 | 英文名 | U+ | 作用 |
| -------- | -------- | -------- | --------- |
| 零宽度空格符 | zero-width space | U+200B | 用于较长单词的换行分隔。 |
| 零宽度非断空格符 | zero width no-break space | U+FEFF | 用于阻止特定位置的换行分隔。|
| 零宽度连字符 | zero-width joiner | U+200D | 用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果。|
| 零宽度断字符 | zero-width non-joiner | U+200C | 用于阿拉伯文、德文、印度语系等文字中,阻止会发生连字的字符间的连字效果。|
| 左至右符 | left-to-right mark |U+200E|用于在混合文字方向的多种语言文本中(例:混合左至右书写的英语与右至左书写的希伯来语),规定排版文字书写方向为左至右。|
| 右至左符 |right-to-left mark|U+200F|用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左。|

使用这些零宽字符可以实现发空白消息、发空白朋友圈等效果。不妨动脑筋一想,我们可以把这几种字符组合起来,就可以实现加密字符串的效果。

# 0x1 基本思路
这篇文章咱们只介绍一个初步实现,说不定以后我会出后续,给应用添加更多功能。下面是加解密的流程图。



总之我认为解密过程较为繁琐。

# 0x2 核心实现
激动人心的时刻到啦。让我们一步步编写。
首先在全局定义三个常量:
```python
ZW_ONE = u"\u200b" # 用来翻译1
ZW_ZERO = u"\u200c" # 用来翻译0
ZW_SEP = u"\u200d" # 用来翻译字符之间的间隔
```
## 加密函数 `str2zwstr(origin)`
首先我们新建一个空数组,用来存储字符串每一项,并且遍历明文,使明文的每个字符**先用`ord()`转换成十进制数字,再用`bin()`转换成二进制。再转换成字符串格式,最后去掉`0b`前缀**:
```python
bin_text = []
for char in origin:
    bin_text.append(str(bin(ord(char))).lstrip('0b'))

```
再创建一个空字符串,用来存储最终的结果:
```python
final_str = ""
```
之后进行两次遍历,先浅层遍历`bin_text`,然后为每一项深层遍历:
```python
for item in bin_text: # 遍历大数组每一项
    for binchar in item: # 遍历每一项中的0和1
      final_str += ZW_ONE if binchar == "1" else ZW_ZERO # 把0、1分别翻译成两种零宽度字符串
    final_str += ZW_SEP # 每一项(字符)结束后,插入一个分隔符号
final_str.rstrip(ZW_SEP) # 去掉
return final_str
```
最后返回`final_str`即可。
整体代码:
```
def str2zwstr(origin):
    bin_text = []
    for char in origin:
      bin_text.append(str(bin(ord(char))).lstrip('0b'))

    final_str = ""
    for item in bin_text:
      for binchar in item:
            final_str += ZW_ONE if binchar == "1" else ZW_ZERO
      final_str += ZW_SEP
    final_str.rstrip(ZW_SEP)
    return final_stry
```

## 解密函数`zwstr2str(enc_str)`
首先我建议大家再看看基本思路中的流程图。解密不大相同。因为一开始我们要把翻译后的数据存储到第一个字符上,但是遇到分隔符后,我们又得新建一个字符,并把接下来的翻译后的数据存储到第二个字符串上,因此我们要编写一个函数`apponlast(arr, sth)`,每次运行,都将sth拼接到arr的最后一项中,如果arr的项数为0,即新增一个元素。

我这里用的代码极为简洁:

```python
def apponlast(arr, sth):
    la = len(arr) # la 是 arr 的长度
    if la: arr += sth # 如果长度不为0,那么就把 arr 的最后一项与 sth 字符串拼接
    else: arr.append(sth) # 如果数组里没有元素,新建一个元素
```
之后编写解密函数:
```python
def zwstr2str(enc_str):
    arr_oz = [] # 由0和1构成的字符串构成的数组
    for char in enc_str: # 在密文里遍历
      if char == ZW_ONE: apponlast(arr_oz, "1") # 如果是\200b,翻译成1
      elif char == ZW_ZERO: apponlast(arr_oz, "0") # 如果是\200c,翻译成0
      elif char == ZW_SEP: arr_oz.append("") # 如果是分隔符,把数组新建一项,重新开始循环
      else: print("Input contains non-ZW string. Aborted."); getinput() # 如果密文中有非零宽字符,终止解密并回到程序主函数(我们一会要编写)

    for idx in range(0, len(arr_oz)-1): # 遍历这个由0和1构成的字符串构成的数组
      arr_oz = chr(int(arr_oz, 2)) # 把每一项先转换为int(注意二进制参数),然后用chr转换为字符

    return "".join(arr_oz) # 拼接
```
# 完善程序
接下来我们为程序添加一个主函数,并且加些花哨的功能。
```python
if __name__ == '__main__':
    pbanner()
    getinput()
```
`pbanner()`用来打印banner:
```python

def pbanner():
    banner = colored(f"""
      ______
   /___/\\Zerowidth String Encoder | @TLHorse from 52pojie
    /// / \\\\ Type in then ENTER. The encoded string
    \\\\ / /__// will be copied & printed.
   \/_____/Commands | ::openweb:: ::banner:: ::quit:: ::switchmode::
    """, 'yellow')
    print(banner)
```
需要说明一下,上面的`colored`函数需要依赖一个第三方库`termcolor`,可以打印出彩色字符串。

我们在全局设置两个变量:
```python
MODE_ENCODE = True # 用来记录模式是加密还是解密
LAST_RESULT = "" # 用来记录上次操作的结果
```

`getinput`是一个递归,可以像命令行一样获取用户输入,代码比较复杂,功能很多,本来是有注释的,结果浏览器编辑的时候不小心给关了,没有恢复成功。大家自己摸索吧:
```python
def getinput():
    global MODE_ENCODE, LAST_RESULT
    info = ""
    if MODE_ENCODE: info = colored("ENCODE", 'red')
    else: info = colored("DECODE", 'green')
    input_str = input(f'[{info}] ')
   
    if   input_str == '::openweb::':    os.system('open https://www.52pojie.cn') # 打开吾爱网页
    elif input_str == '::banner::':   pbanner() #打印banner
    elif input_str == '::quit::':       sys.exit(0) #退出程序
    elif input_str == '::switchmode::': MODE_ENCODE = False if MODE_ENCODE else True #切换加解密
    elif input_str == '::cp::':         os.system(f'echo {LAST_RESULT} | pbcopy') #复制结果

    if input_str.startswith("::") and input_str.endswith("::"): getinput() #检测是否为命令
   
    out = str2zwstr(input_str) if MODE_ENCODE == True else zwstr2str(input_str)
    print(colored('   >>> "', 'green')+ out + colored('"', 'green'))
    LAST_RESULT = out
    getinput()
```
别忘了import进类库:
```python
import os, sys
from termcolor import colored
```
大功告成!

# 后记
首先,想说明一点,文中的结果复制功能是基于pbcopy命令的,这个只有Linux和Unix有,Windows没有。所以Windows小伙伴们记得使用`pyperclip`库实现复制功能。

其次,我也不是程序员,所以代码的繁琐与不妥当之处欢迎跟帖指正。

最后,这篇文章很有可能会出续集哦!
所有代码长这样:
```python
import os, sys
from termcolor import colored

ZW_ONE = u"\u200b"
ZW_ZERO = u"\u200c"
ZW_SEP = u"\u200d"

MODE_ENCODE = True
LAST_RESULT = ""

def pbanner():
    banner = colored(f"""
      ______
   /___/\\Zerowidth String Encoder | @TLHorse from 52pojie
    /// / \\\\ Type in then ENTER. The encoded string will be printed.
    \\\\ / /__// Commands | ::openweb:: ::banner:: ::quit:: ::switchmode::
   \/_____/::cp::
    """, 'yellow')
    print(banner)

def apponlast(arr, sth):
    la = len(arr)
    if la: arr += sth
    else: arr.append(sth)

def str2zwstr(origin):
    bin_text = []
    for char in origin:
      bin_text.append(str(bin(ord(char))).lstrip('0b'))

    final_str = ""
    for item in bin_text:
      for binchar in item:
            final_str += ZW_ONE if binchar == "1" else ZW_ZERO
      final_str += ZW_SEP
    final_str.rstrip(ZW_SEP)
    return final_str

def zwstr2str(enc_str):
    arr_oz = []
    for char in enc_str:
      if char == ZW_ONE: apponlast(arr_oz, "1")
      elif char == ZW_ZERO: apponlast(arr_oz, "0")
      elif char == ZW_SEP: arr_oz.append("")
      else: print("Input contains non-ZW string. Aborted."); getinput()

    for idx in range(0, len(arr_oz)-1):
      arr_oz = chr(int(arr_oz, 2))

    return "".join(arr_oz)

def getinput():
    global MODE_ENCODE, LAST_RESULT
    info = ""
    if MODE_ENCODE: info = colored("ENCODE", 'red')
    else: info = colored("DECODE", 'green')
    input_str = input(f'[{info}] ')
   
    if   input_str == '::openweb::':    os.system('open https://www.52pojie.cn')
    elif input_str == '::banner::':   pbanner()
    elif input_str == '::quit::':       sys.exit(0)
    elif input_str == '::switchmode::': MODE_ENCODE = False if MODE_ENCODE else True
    elif input_str == '::cp::':         os.system(f'echo {LAST_RESULT} | pbcopy')

    if input_str.startswith("::") and input_str.endswith("::"): getinput()
   
    out = str2zwstr(input_str) if MODE_ENCODE == True else zwstr2str(input_str)
    print(colored('   >>> "', 'green')+ out + colored('"', 'green'))
    LAST_RESULT = out
    getinput()

if __name__ == '__main__':
    pbanner()
    getinput()
```
代码下载在这里:链接:https://share.weiyun.com/PyUOPC6F 密码:zsetlh

xiahhhr 发表于 2021-1-22 09:48

有趣的代码,哈哈哈

wuai_leader 发表于 2021-1-22 10:07

感觉可以完成那种普通编辑器打开是一篇文章,这个程序打开是信息的那种加密
有点像谍战片里面的一本书是密码本那种{:301_1009:}

thinkingbullet1 发表于 2021-1-22 10:10

思路清奇!

笙若 发表于 2021-1-22 13:15

这样加密的话体积是不是变成原来的64倍了

helian147 发表于 2021-1-24 18:15

\u200b就在vx上碰到过,下载下来保存不了,仔细看就有它{:1_908:}
页: [1]
查看完整版本: 使用零宽字符对文本加密的实现