This algorithm is designed & implemented by JieJiSS.

Anyway, 你说英文写博客好，那我就试试呗

## 0x00 Basic Variables & Helper Functions

Define raw: The variable raw is used to represent the raw string constant which needs to be encoded. Beware that this is not an encryption algorithm, which means that it is not cryptographically secure.

### 0x00.1 VariabledataArr: number[]

This is a pre-generated array which stores at least 9 (1+2+2*2+2) elements.

Define $m$ as: $m = \max\,(2, \lceil\frac{\texttt{raw.length}}{7}\rceil)$

Magic number 7: Every ASCII string that has no more than 7 characters can be encoded to a base64 string that has a maximum length of 10 characters (exclude trailing equal signs), which can be safely treated as a 36-decimal integer without any precision lost under the IEEE754 double data type. That's because in that case, parseInt(encodedStr, 36) always returns an integer that is lower than Number.MAX_SAFE_INTEGER, i.e. $2^{53}-1=9007199254740991$, since 9007199254740991 .toString(36) returns "2gosa7pa2gv" (which has 11 characters).

### 0x00.2 Helperf(x)

This helper function is used to calculate the "system" of the x-th element of dataArr. f(x) could be any function which satisfied following criteria:

1. $f(0) \equiv 10 \mod 256$
2. $f(1) \equiv 36 \mod 256$
3. $f(2) \equiv 2 \mod 256$
4. $f(3) \equiv 2 \mod 256$
5. $\lfloor f(k) \rfloor =f(k) \quad(k\in\mathbb N^*)$
6. $f(k)>0\quad (k=0,1,2,3)$

Here are some available f(x)s: \begin{aligned} f_1(x)&=-27x^3+179x^2-126x+10\\ f_2(x)&=101x^3-333x^2+258x+266 \end{aligned}

How to create a valid $f(x)$?

Let $f(x)=ax^3+bx^2+cx+d$.

According to the first criterion, we know $f(0)=d \equiv 10 \mod 256$.

So, we can simply choose values that look good for $d$: 10, 266, 522, …

For similar reasons, we know that $f(1)-d=a+b+c\equiv 26\mod 256$, $f(2)-d=8a+4b+2c\equiv 248\mod 256$, and $f(3)-d=27a+9b+3c \equiv 248\mod 256$. Note that $27a+9b+3c\equiv 0\mod 3$, thus in order to get integer values for $a$, $b$, and $c$, we need to ensure that $27a+9b+3c=248+(3k+1)\times256$. Detailed explanation is that $248=3\times82+2$ and $256=3\times85+1$, hence $248+256+3k\times256 = 504+3k\times256$ can always be divided by 3. After these calculations, we can simply solve the ternary systems of equations and get a possible combination for $a$, $b$ and $c$.

To quickly solve such systems of equations, you may need to know how to use the Gaussian Elimination.

Cited from https://yutsumura.com/solving-a-system-of-linear-equations-using-gaussian-elimination/: \begin{align*} x+2y+3z &=4 \\ 5x+6y+7z &=8\\ 9x+10y+11z &=12 \end{align*} The three elementary row operations on a matrix are defined as follows.

1. Interchanging two rows: $R_i \leftrightarrow R_j$ interchanges rows $i$ and $j$.

2. Multiplying a row by a non-zero scalar (a number):

$t R_i$ multiplies row ii by the non-zero scalar (number) $t$.

3. Adding a multiple of one row to another row:

$R_j+tR_i$ adds $t$ times row $i$ to row $j$.

Solution

First, the augmented matrix of the system is: $A=\left[ \begin{array}{rrr|r} 1 & 2 & 3 & 4 \\ 5 & 6 & 7 & 8 \\ 9 & 10 & 11 & 12 \end{array} \right].$ We apply elementary row operations as follows to reduce the system to row echelon form. $A \xrightarrow{R_3 -9R_1} \left[\begin{array}{rrr|r} 1 & 2 & 3 & 4 \\ 5 & 6 & 7 & 8 \\ 0 & -8 & -16 & -24 \end{array}\right] \xrightarrow{-\frac{1}{8}R_3} \left[\begin{array}{rrr|r} 1 & 2 & 3 & 4 \\ 5 & 6 & 7 & 8 \\ 0 & 1 & 2 & 3 \end{array}\right]$

$\xrightarrow{R_2-5R_1} \left[\begin{array}{rrr|r} 1 & 2 & 3 & 4 \\ 0 & -4 & -8 & -12 \\ 0 & 1 & 2 & 3 \end{array}\right] \xrightarrow{-\frac{1}{4} R_2} \left[\begin{array}{rrr|r} 1 & 2 & 3 & 4 \\ 0 & 1 & 2 & 3 \\ 0 & 1 & 2 & 3 \end{array}\right]$

$\xrightarrow{R_3-R_2} \left[\begin{array}{rrr|r} 1 & 2 & 3 & 4 \\ 0 & 1 & 2 & 3 \\ 0 & 0 & 0 & 0 \end{array}\right]$

The last matrix is in row echelon form.

The corresponding system of linear equations of it is: \begin{align*} x+2y+3z &=4\\ y+2z&=3 \\ 0z&=0 \end{align*}

### 0x00.3 Helperb64(str)

This helper function encodes str to a modified-base64 string literal. Note that it should remove all trailing equal signs. Also, note that the whole b64templ string except its last three characters is reversed to annoy browsers' built-in atob base64 decode function.

### 0x00.4 Helperdeb64(str)

Decode base64 encoded str.

### 0x00.5 HelperrndChar()

This helper function is used to generate a random character ranged in a-zA-Z0-9.

### 0x00.6 HelperpadLeft(str, len, ch)

This helper function will repeatedly add ch as left padding to str until str.length === len.

## 0x01 Generator

For instance, we'd like to hide a string constant "Hello, World!" from users.

Step 1. Slice this string constant into $m$ pieces:

Now, we have an array sliced: string[] which contains sliced parts of the string constant:

Step 2. Record the letter cases of all characters in sliced: string[]. Also, in this step we will need to encode the lowercase of each string element's of sliced using the b64() helper function, and store it in partial: string[].

In this case, rb64encodedText = ["r3oRi3BR19", "oHALi3tc"][i].

Now, cases becomes [76, 112] ([0b1001100, 0b1110000], was once [0b0001001100, 0b01110000]), and partial becomes["r3ori3br19", "ohali3tc"]. You may have noticed that the 0s before the first 1 won't affect the result, so we'll use the padLeft helper function to retrieve those missing 0s later when we're attempting to decode it.

Step 3. Replace all +/= in partial[i] to random characters ranged in a-zA-Z0-9, and store relative information in special: number[]. This step makes sure that parseInt(partial[i], 36) can always return a valid integer (stored inside magicNums: number[]).

According to Base64 - Wikipedia, equal signs only appear at the end of a base64 encoded string, so there's no need to deal with it because we have trimmed trailing equal signs in former steps.

Note that on line 4, we give chunks: string[] an initial value ["0", "0"]. That's because parseInt("") returns NaN, and in most cases base64Text won't include "+" or "/", so chunks will likely to remain unmodified. Hence, we need to make sure parseInt(chunks[i]) will not return NaN.

Now, special becomes [0,0,0,0] since there's no + or / presents in base64-encoded text, and magicNums becomes [2752522766809245, 1918386016464].

Step 4. Concat magicNums.length, magicNums, special and cases together, and store it as dataArr. Then, convert dataArr to data.

I've recognized that symbols like + and / rarely appears in the base64 encode method, so there'll always be a huge amount of 0s in special: number[]. Thus, if we convert all the 0s in dataArr to "", we can save quite a lot of bytes.

Conclusion for Generator Part

Summary all codes together:

And the input is encoded as:

## 0x02 Decoder

Assume that we have fetched the encoded data from our backend:

Our backend codes:

Our frontend codes:

And now, we need to apply several operations to the encoded variable in order to decode it.

First of all, we will convert encoded: string to an array named firstStepArr:

And then, we'll try slice this array into 3 parts. Since the encoded str contains three parts, magicNums, special and cases, we can have a glance at previous codes:

As we can see, let $l=\texttt{sliced.length}$, then we have $\texttt{partial.length}=l$ and $\texttt{cases.length}=l$. Also, we can infer that $\texttt{special.length}=2l$ and $\texttt{magicNums.length}=l$. Thus, we can easily know that $\texttt{firstStepArr.length}=4l+1$.

So, we can declare a size: number variable as we will need to use it to slice the firstStepArr and generate a secondStepArr: number[][] later:

In the third step, we try to convert numbers to decoded strings. The $f(x)$ function mentioned above now shows its necessity:

Yep, we now have the raw text decoded as thirdStepArr!

Conclusion for Decoder Part

Final codes:

The decoded result:

## 0x03 Use Without Decoder

Well, you can encode users' input and compare it with the already encoded text. However, you will have to modify rndChar to achieve that, since in some rarely seen cases it will cause different encoded result for the same input text.

My suggestion is that you can use a controllable PRNG (Pseudo Random Number Generator) to replace Math.random(), for example:

Don't forget to call resetAll() to reset exists to [seed % modulo] and index to -1 every time you want to encrypt a string!