|
| 1 | +# -*- coding: UTF-8 -*- |
| 2 | +"""Floating-point block ciphers, shared by the toy ciphers of online JavaScript encryption tools (AER-256, ARMON-64). |
| 3 | +
|
| 4 | +The text is split into blocks of half the key length (rounded up), each block being read as a big-endian number of its |
| 5 | + character codes. For each character of the key (at least 3), an operation with its code is applied to this number in |
| 6 | + turn (in floating-point arithmetic, cycling through the operations of the cipher), and the resulting numbers are |
| 7 | + written as JavaScript would (e.g. "123.456") and joined with a separator. Decryption applies the inverse operations in |
| 8 | + reverse order and rounds the result. As numbers are double-precision floats, blocks of more than 6 characters (i.e. |
| 9 | + keys of more than 12 characters) lose precision and generally cannot be fully decrypted. Only characters with a code |
| 10 | + below 256 can be encrypted, and null characters at the start of a block are lost. |
| 11 | +""" |
| 12 | +import re |
| 13 | +from math import floor, isfinite |
| 14 | + |
| 15 | +from ..__common__ import ensure_str, handle_error |
| 16 | + |
| 17 | + |
| 18 | +# operations of the ciphers with their inverses, as (encryption, decryption) pairs |
| 19 | +OPERATIONS = [ |
| 20 | + (lambda v, k: v + k, lambda v, k: v - k), |
| 21 | + (lambda v, k: v / k, lambda v, k: v * k), |
| 22 | + (lambda v, k: v - k, lambda v, k: v + k), |
| 23 | + (lambda v, k: v * (.01 * k), lambda v, k: v / (.01 * k)), |
| 24 | +] |
| 25 | + |
| 26 | + |
| 27 | +def js_str(x): |
| 28 | + """ Convert a float to a string like JavaScript's Number.prototype.toString. """ |
| 29 | + x = float(x) |
| 30 | + if x != x: |
| 31 | + return "NaN" |
| 32 | + if x < 0: |
| 33 | + return "-" + js_str(-x) |
| 34 | + if x == 0: |
| 35 | + return "0" |
| 36 | + if not isfinite(x): |
| 37 | + return "Infinity" |
| 38 | + # shortest digits that round-trip (as with repr) and decimal exponent n such that x = 0.digits * 10^n |
| 39 | + m, _, e = repr(x).partition("e") |
| 40 | + i, _, f = m.partition(".") |
| 41 | + d = (i + f.rstrip("0")) if f != "0" else i |
| 42 | + n = len(i) + int(e or 0) |
| 43 | + k = len(d.lstrip("0")) |
| 44 | + n -= len(d) - k |
| 45 | + d = d.lstrip("0") |
| 46 | + if k <= n <= 21: |
| 47 | + return d + "0" * (n - k) |
| 48 | + if 0 < n <= 21: |
| 49 | + return d[:n] + "." + d[n:] |
| 50 | + if -6 < n <= 0: |
| 51 | + return "0." + "0" * -n + d |
| 52 | + return d[0] + ("." + d[1:] if k > 1 else "") + "e" + "+-"[n < 1] + str(abs(n - 1)) |
| 53 | + |
| 54 | + |
| 55 | +def float_cipher(name, sep, n_ops): |
| 56 | + """ Make the encoding and decoding function factories of a cipher, given its separator and its number of |
| 57 | + operations. """ |
| 58 | + # the separator is not matched inside numbers in exponent notation (e.g. "1e+21") |
| 59 | + split = re.compile(r"(?<![eE])" + re.escape(sep)) |
| 60 | + |
| 61 | + def _key(key): |
| 62 | + key = str(key or "") |
| 63 | + if len(key) < 3: |
| 64 | + raise LookupError(f"Bad parameter for encoding '{name}': key must be at least 3 characters long") |
| 65 | + return [ord(c) for c in key], (len(key) + 1) // 2 |
| 66 | + |
| 67 | + def encode_factory(key=None): |
| 68 | + def encode(text, errors="strict"): |
| 69 | + k, n = _key(key) |
| 70 | + _h, t = handle_error(name, errors), ensure_str(text) |
| 71 | + t = "".join(c for c in "".join(c if ord(c) < 256 else _h(c, i) for i, c in enumerate(t)) if ord(c) < 256) |
| 72 | + r = [] |
| 73 | + for i in range(0, len(t), n): |
| 74 | + v = float(int("".join(f"{ord(c):02x}" for c in t[i:i+n]), 16)) |
| 75 | + for j, kc in enumerate(k): |
| 76 | + v = OPERATIONS[j % n_ops][0](v, kc) |
| 77 | + r.append(js_str(v)) |
| 78 | + return sep.join(r), len(text) |
| 79 | + return encode |
| 80 | + |
| 81 | + def decode_factory(key=None): |
| 82 | + def decode(text, errors="strict"): |
| 83 | + k, _ = _key(key) |
| 84 | + _h, t, r = handle_error(name, errors, decode=True, kind="number"), ensure_str(text), "" |
| 85 | + for i, s in enumerate(split.split(t) if t else []): |
| 86 | + try: |
| 87 | + v = float(s) |
| 88 | + except ValueError: |
| 89 | + r += _h(s, i) |
| 90 | + continue |
| 91 | + for j in range(len(k) - 1, -1, -1): |
| 92 | + v = OPERATIONS[j % n_ops][1](v, k[j]) |
| 93 | + # Math.round, then the hexadecimal digits of the number (nothing when it is not positive) |
| 94 | + v = floor(v) + (v - floor(v) >= .5) if isfinite(v) else 0 |
| 95 | + h = f"{v:x}" if v > 0 else "" |
| 96 | + h = "0" * (len(h) % 2) + h |
| 97 | + r += "".join(chr(int(h[j:j+2], 16)) for j in range(0, len(h), 2)) |
| 98 | + return r, len(text) |
| 99 | + return decode |
| 100 | + |
| 101 | + return encode_factory, decode_factory |
0 commit comments