跳至正文
LNN的博客!

Richard写的文本加密算法

Richard在他的新作品中使用一种可逆的加密算法来把源代码中的文本加密成乱码。其实质很简单,类似于异或加密——把字符编码‌“最高的一个有效位”‌(即最高的一个1)以下的每一位都反转,如:

     $554A 啊
 V最高有效位
01010101 01001010
VV|||||| ||||||||
01101010 10110101
     $6AB5 檵
     $0052 R
          V最高有效位
00000000 01010010
VVVVVVVV VV||||||
00000000 01101101
     $006D m

原实现

虽然算法很简单,但由于历史原因,该算法的实现十分阴间。这是Python源代码(我调整了函数的顺序):

def sts(str1):
    b=""
    a=stn(str1)
    x=1
    for i in str(a):
        if x==1:
            b+=i
            x=0
        elif i=="0":
            b+="1"
        elif i=="1":
            b+="0"
        else:
            b+="2"
            x=1
    str2=nts(b)
    return str2

def nts(a):
    str1=""
    ls=[]
    for k in str(a):
        if k=="2":
            ls.append(int(str1))
            str1=""
        else:
            str1+=k
    b=""
    for i in ls:
        b+=chr(num2t10(i))
    return b

def num2t10(num):
    num=str(num)
    num1=""
    num2=0
    for i in range(len(num)):
        num1+=num[-i-1]
    for j in range(len(num1)):
        num2+=int(num1[j])*(2**j)
    return num2

def stn(str1):
    a=""
    for i in str1:
        a+=str((num10t2(ord(i))))
        a+="2"
    return int(a)

def num10t2(num):
    str1=""
    str2=""
    while num>0:
        c=num%2
        str1+=str(c)
        num=int(num/2)
    for i in range(len(str1)):
        str2+=str1[-i-1]
    return int(str2)

为了方便理解,这是我修改了变量名的版本:

def sts(origin): # OPTIMIZE
    """加密/解密主函数。
    
    由于实质上是异或加密,所以加密和解密是同一个算法。
    """
    resultEncoded = ""
    originEncoded = str(stn(origin)) # 第一步是调用stn(),先看stn()的定义
    isHighestBit = True
    for bit in originEncoded: # 这个循环用来进行迫真按位运算
        if isHighestBit:
            resultEncoded += bit
            isHighestBit = False
        elif bit == '0':
            resultEncoded += '1'
        elif bit == '1':
            resultEncoded += '0'
        else:
            resultEncoded += '2'
            isHighestBit = True
    return nts(resultEncoded) # 最后调用nts(),看nts()的定义

def stn(string):
    """把每个字符的字符编码转换成二进制数,然后每个数后用'2'连接。
    
    如:"ABC""100000121000010210000112"
    """
    # 实际上,一开始采用的加密算法是这个函数;
    # 由于密文的体积太大,才有了现在的版本。
    encoded = ""
    for char in string:
        encoded += str(num10t2(ord(char))); # 对char的代码点调用了num10t2()
        encoded += '2'
    return int(encoded) # ???: 为什么要转成int???

def num10t2(decimal):
    """把正整数用二进制表示。
    """
    reversedBinary = ""
    binary = ""
    while decimal > 0:
        reversedBinary += str(decimal % 2) # OPTIMIZE: 建议改成:decimal & 1
        # OPTIMIZE: 为什么要事后手动倒转字符串,而不是直接把这里新增的字符拼接在字符串的开头?
        decimal = int(decimal / 2) # OPTIMIZE: decimal >>= 1 它不香吗?
    for i in range(len(reversedBinary)):
        binary += reversedBinary[len(reversedBinary) - i - 1]
    return int(binary) # ???: 为什么要转成int???

def nts(encoded):
    """解码“encode()”生成的密文。
    """
    binaryBuffer = "" # 存储一个二进制数,遇到'2'就拿走使用并清空
    binaryList = []
    for bit in str(encoded):
        if bit == '2':
            binaryList.append(int(binaryBuffer)) # HACK: 为什么要转成int???
            binaryBuffer = ""
        else:
            binaryBuffer += bit
    decoded = ""
    for binary in binaryList:
        decoded += chr(num2t10(binary)) # 对代码点的二进制调用了num2t10()
    return decoded

def num2t10(binary):
    """解析二进制数。
    """
    # OPTIMIZE
    # 这算法看得我人都傻了
    binary = str(binary)
    reversedBinary = ""
    decimal = 0
    for i in range(len(binary)):
        reversedBinary += binary[len(binary) - i - 1]
    for i in range(len(reversedBinary)):
        decimal += int(reversedBinary[i]) * pow(2, i) # 1 << i 它不香吗?
        # 就算要乘方,** 运算符不知道??
    return decimal

我必须吐槽num10t2num2t10这两个函数名,人家整数本来就是按二进制存储的,显示成十进制只是为了给人看的;况且好好的按位运算,非要用字符串来实现,白拼接那么多次字符串。于是我重写了一下这个算法。

我重写的算法

JS:

function sts(string) {
    var out = "";
    [...string].forEach(char => {
        let codePoint = char.codePointAt(0),
            shifter = -1,
            temp = codePoint;
        while (temp & -2) {
            temp >>= 1;
            shifter <<= 1;
        }
        out += String.fromCodePoint(codePoint ^ ~shifter);
    });
    return out;
}

Richard只会Python,所以又给他写了个Python版:

def sts(string):
    out = ""
    for char in string:
        codePoint = ord(char)
        shifter = -1
        temp = codePoint
        while temp & -2:
            temp >>= 1
            shifter <<= 1
        out += chr(codePoint ^ ~shifter)
    return out

又写了个Ruby版练手:

def sts(string)
    out = ""
    string.each_codepoint do |codePoint|
        shifter = -1
        temp = codePoint
        until (temp & -2).zero?
            temp >>= 1
            shifter <<= 1
        end
        out << (codePoint ^ ~shifter)
    end
    out
end

顺便用按位运算重新实现了num10t2()num2t10()

def fromBin(bin):
    assert isinstance(bin, str) # 永远不要把二进制数转换成int!!
    dec = 0
    for i in bin:
        dec <<= 1
        if i == "1":
            dec |= 1
    return dec

def toBin(dec):
    assert isinstance(dec, int)
    assert dec > 0
    bin = ""
    while dec:
        bin = str(dec & 1) + bin # 把新的数位追加到bin的开头
        dec >>= 1
    return bin

2023-08-12 更新

经过我的学习,sts 算法现已有更好的实现。

改良 sts 第二代

def sts(string):
    out = ""
    for char in string:
        codePoint = ord(char)
        bits = codePoint >> 1
        bits |= bits >> 1
        bits |= bits >> 2
        bits |= bits >> 4
        bits |= bits >> 8
        bits |= bits >> 16
        out += chr(codePoint ^ bits)
    return out
def sts(string)
  out = String.new(encoding: "UTF-8")
  string.each_codepoint do |codePoint|
    bits = codePoint >> 1
    bits |= bits >> 1
    bits |= bits >> 2
    bits |= bits >> 4
    bits |= bits >> 8
    bits |= bits >> 16
    out << (codePoint ^ bits)
  end
  out
end
function sts(string) {
  let out = ""
  for (let char of string) {
    const codePoint = char.codePointAt(0)
    let bits = codePoint >> 1
    bits |= bits >> 1
    bits |= bits >> 2
    bits |= bits >> 4
    bits |= bits >> 8
    bits |= bits >> 16
    out += String.fromCodePoint(codePoint ^ bits)
  }
  return out
}
local bit32 = require("bit32")
local ustring = mw.ustring

local function sts(str)
  local out = ""
  for codePoint in ustring.gcodepoint(str) do
    local bits = bit32.rshift(codePoint, 1)
    bits = bit32.bor(bits, bit32.rshift(bits, 1))
    bits = bit32.bor(bits, bit32.rshift(bits, 2))
    bits = bit32.bor(bits, bit32.rshift(bits, 4))
    bits = bit32.bor(bits, bit32.rshift(bits, 8))
    bits = bit32.bor(bits, bit32.rshift(bits, 16))
    out = out .. ustring.char(bit32.bxor(codePoint, bits))
  end
  return out
end

运用类似 MSB(最高有效位)算法的一系列按位运算,就可以得到字符编码中需要反转的位掩码(bits,也是第一版改良实现中的 ~shifter),无需使用之前的 while 循环。这种 MSB 计算方法要求操作是 32 位整数,否则需要调整 bits |= bits >> N 语句的数量;不过因为字符编码最多超不过 21 位,所以实际上不用担心。

此版本虽然在 Python、Ruby 领域已被下文的新版本取代,但在使用 JavaScript、Lua 等没有(或由于某些原因选择不使用)可变长度整数或原生 MSB 函数的编程语言实现时,仍然是最优的方法。

改良 sts 第三代

def sts(string):
    out = ""
    for char in string:
        codePoint = ord(char)
        out += chr(codePoint ^ ~(-1 << (codePoint.bit_length() - 1)))
    return out
def sts(string)
  out = String.new(encoding: "UTF-8")
  string.each_codepoint do |codePoint|
    out << (codePoint ^ ~(-1 << (codePoint.bit_length - 1)))
  end
  out
end

Python、Ruby 中的整型是可变长度的,可以直接通过其 bit_length 方法求得长度,于是获得刚才所说的 bits 就更简单了,它就是 ~(-1 << (cp.bit_length() - 1)),或 (1 << (cp.bit_length() - 1)) - 1

速度测试

在 Python 中用四个版本的 sts 对这段示例文本

经过两个多月的网课,疫情基本结束,学校终于要复课了。

2020.4.20。复课的第一天,我满怀期待地走进了校门。熟悉的景象在我眼前展开,只是所有人都戴了个口罩。顶着大风,仿佛回到了二三月份。

走在我前面的是我的8B班同学Richard。三个多月没看见他,他在我眼中更加高大了。我在网课时曾屡次想念过他,现在,我可以以学习为理由来接近他,和他在一起。

我在上个学期观察过他,他很少和Sunny说话,Sunny也没有去主动找他。但我猜不出他是否已经放弃了Sunny。机不可失时不再来,我得在结课考之前追求到他。

分别进行加密操作测试,结果如下:

版本 实验次数 平均用时(毫秒)
Richard 原版 500 5.688
改良一代 5000 0.595
改良二代 50000 0.192
改良三代 50000 0.116

2023-11-18 更新

8 月 28 日,我发现 sts 算法具有一个问题:由于字符编码 D800DFFF 为 UTF-16 代理字,用于在 UTF-16 中编码 U+10000 及以上的字符,这个范围内的编码不是合法的 Unicode 字符;这意味着 A000A7FF 范围内的字符经过 sts 编码后,会得到不正确的 Unicode 文本;轻则出现问号替代字符,重则直接报错。

不过,A000A7FF 范围内的字符显然完全不是我们会用到的(见下表),上述问题其实可以不用解决。考虑这个问题只是因为太闲了为了严谨。

范围 名称 英文名称
U+A000..U+A48F 彝文音节 Yi Syllables
U+A490..U+A4CF 彝文部首 Yi Radicals
U+A4D0..U+A4FF 老傈僳文 Lisu
U+A500..U+A63F 瓦伊语文字 Vai
U+A640..U+A69F 西里尔字母扩展-B Cyrillic Extended-B
U+A6A0..U+A6FF 巴姆穆文字 Bamum
U+A700..U+A71F 声调修饰符号 Modifier Tone Letters
U+A720..U+A7FF 拉丁文扩展-D Latin Extended-D

在 Python 实现中,只要不试图将包含 UTF-16 代理字的密文以默认的 UTF-8 编码存入文件或直接输出,就不会出现异常,因为 Python 中的字符串是以代码点为单位存储,而不是以编码过的字节序列形式存储的,只有在输出、写入到文件等操作的过程中,字符串才会被编码。如果确实需要将 sts 密文保存成文件,可以考虑自己实现 UTF-8 编解码。

后来我发现这种编码方法还有另一个问题:Unicode 第 16 平面上的字符(U+100000..U+10FFFF)是肯定没办法编码的,因为这个范围经过 sts 会变成 U+1F0000..U+1FFFFF,是根本不存在的。第 16 平面内的字符全部属于补充私人使用区-BSupplementary Private Use Area-B),如此偏远的区段基本很少会用到;但由于问题的存在,sts 目前的所有实现都已经不完美了。

为了解决以上问题,我们可以直接在 UTF-8 编码的字节序列的层面上编写 sts 算法。

def sts_utf8(string)
  bytes = string.bytes
  zero = false
  bytes.map! do |byte|
    bits =
      case
      when byte >= 0xf8 then raise ArgumentError, "invalid byte sequence in UTF-8"
      when byte >= 0xf0 then        byte & 0x07
      when byte >= 0xe0 then        byte & 0x0f
      when byte >= 0xc0 then        byte & 0x1f
      when byte >= 0x80 then zero ? byte & 0x3f : 0x40
      else                          byte
      end
    zero = bits.zero?
    byte ^ ~(-1 << (bits.bit_length - 1))
  end
  bytes.pack("C*")
end

def sts_encode(string)
  sts_utf8(string.encode("UTF-8"))
end

def sts_decode(string)
  sts_utf8(string).force_encoding("UTF-8")
end
local bit32 = require("bit32")

local function sts(str)
  local bytes = { str:byte(1, #str) }
  local zero = false
  for i, byte in ipairs(bytes) do
    local bits
    if     byte >= 0xf8 then error("bad utf-8 string in 'sts'")
    elseif byte >= 0xf0 then bits = bit32.band(byte, 0x07)
    elseif byte >= 0xe0 then bits = bit32.band(byte, 0x0f)
    elseif byte >= 0xc0 then bits = bit32.band(byte, 0x1f)
    elseif byte >= 0x80 then
      if zero then           bits = bit32.band(byte, 0x3f)
      else                   bits = 0x40
      end
    else                     bits = byte
    end
    zero = bits == 0

    bits = bit32.rshift(bits, 1)
    bits = bit32.bor(bits, bit32.rshift(bits, 1))
    bits = bit32.bor(bits, bit32.rshift(bits, 2))
    bits = bit32.bor(bits, bit32.rshift(bits, 4))
    bytes[i] = bit32.bxor(byte, bits)
  end
  return string.char(unpack(bytes))
end
def sts_utf8(data: bytes):
    out = []
    zero = False
    for byte in data:
        if   byte >= 0xf8: raise UnicodeDecodeError("invalid start byte")
        elif byte >= 0xf0: bits = byte & 0x07
        elif byte >= 0xe0: bits = byte & 0x0f
        elif byte >= 0xc0: bits = byte & 0x1f
        elif byte >= 0x80: bits = byte & 0x3f if zero else 0x40
        else:              bits = byte
        zero = bits == 0
        out.append(byte ^ ~(-1 << (bits.bit_length() - 1)))
    return bytes(*out)

def sts_encode(string: str):
    return sts_utf8(string.encode(encoding='utf-8'))

def sts_decode(data: bytes):
    return sts_utf8(data).decode(encoding='utf-8')
/** @type {TextEncoder} */
let textEncoder
/** @type {TextDecoder} */
let textDecoder

/** @param {Uint8Array} bytes */
function stsUTF8(bytes) {
  bytes = bytes.slice(0)
  let zero = false
  for (let i = 0; i < bytes.length; i++) {
    const byte = bytes[i]
    let bits = byte
    if      (byte >= 0xf8) throw new RangeError("Invalid byte")
    else if (byte >= 0xf0) bits =        byte & 0x07
    else if (byte >= 0xe0) bits =        byte & 0x0f
    else if (byte >= 0xc0) bits =        byte & 0x1f
    else if (byte >= 0x80) bits = zero ? byte & 0x3f : 0x40
    zero = bits === 0

    bits >>= 1
    bits |= bits >> 1
    bits |= bits >> 2
    bits |= bits >> 4
    bytes[i] = byte ^ bits
  }
  return bytes
}

/** @param {string} string */
function stsEncode(string) {
  textEncoder ||= new TextEncoder()
  return stsUTF8(textEncoder.encode(string))
}

/** @param {Uint8Array} bytes */
function stsDecode(bytes) {
  textDecoder ||= new TextDecoder()
  return textDecoder.decode(stsUTF8(bytes))
}

另外,以 UTF-16 为基础进行编码也未尝不是一种思路。在 JavaScript 中实现这种方法尤为容易:

/** @param {string} string */
function stsUTF16(string) {
  let out = ""
  for (let i = 0; i < string.length; i++) {
    const charCode = string.charCodeAt(i)
    let bits = charCode >> 1
    bits |= bits >> 1
    bits |= bits >> 2
    bits |= bits >> 4
    bits |= bits >> 8
    out += String.fromCharCode(charCode ^ bits)
  }
  return out
}

评论区

加载基于 GitHub issues 的 utteranc.es 评论区组件……