I. Application scenarios

Take YouTube’s video details page for example:

    https://www.youtube.com/watch?v=8dUpL8SCO1w
Copy the code

Each video has a unique primary key in the database, but 8dUpL8SCO1w here is not the primary key.

    https://www.youtube.com/watch?v=10002
Copy the code

If the primary key is exposed directly on the URL of the video detail, it is easy to guess that it is the database primary key.

Under normal circumstances, the primary key of the database is an auto-increment number, which has the following advantages: 1. It occupies small space and saves CPU overhead. 2. The primary key is usually indexed. The integer index can load more memory and has better performance.

How do you protect the database primary key to avoid this problem?

2. Features of Hashids

The name Hashids is reminiscent of hash functions, but hash functions can only be encrypted, not decrypted.

The well-known encryption and decryption algorithms are as follows:

  • Symmetric encryption: DES, 3DES, etc.
  • Asymmetric encryption: RSA, ECC, etc.

However, the above scenarios do not require such a high level of security. The cost and efficiency of symmetric or asymmetric encryption need to be considered.

In addition, it is easy to overlook the problem that the encrypted primary key is placed in the URL, and the length is also a standard to measure.

The basic features of the Hashids library are as follows:

  • Encryption and decryption processing with some security
  • Generate an unpredictably unique short ID

Generate short ID

Hashids only supports numeric inputs, mainly because it is intended to clarify its application scenarios and does not want developers to use it as an encryption and decryption library.

The way to convert numbers to shorter strings is “base conversion”.

For example, when binary 1010 is converted to 10 in decimal, its length is reduced by 2.

export const toAlphabet = ( input: NumberLike, alphabetChars: string[], ): string[] => { const id: string[] = [] let value = input if (typeof value === 'bigint') { const alphabetLength = BigInt(alphabetChars.length) do { id.unshift([Number(value % alphabetLength)]) value /= alphabetLength } while (value > BigInt(0)) } else { do { // Default decimal conversion to sixty binary ID.unshift (alphabetChars[value % alphabetchars.length]) value = math.floor (value/alphabetchars.length)}  while (value > 0) } return id }Copy the code

The Hashids used the above method to convert the input value to sixty bits (alphabetChars defaults to 62 characters), which greatly reduces the length of the original number.

Careful students will notice that alphabetChars is a one-to-one mapping to the final output, so alphabetChars must be an encrypted array.

4. Modified Fisher-Yates Shuffle algorithm

Fisher — Yates Shuffle is an algorithm that generates a random sequence from a finite set.

The Fisher-Yates shuffle algorithm randomly selects numbers every time, so it cannot be used directly. As a result, the encrypted contents in the same scenario are different.

export function shuffle( alphabetChars: string[], saltChars: string[], ): string[] { if (saltChars.length === 0) { return alphabetChars } let integer: number const transformed = [...alphabetChars] for (let i = transformed.length - 1, v = 0, p = 0; i > 0; I --, v++) {v %= saltChars. Length p += INTEGER = saltChars[v]. CodePointAt (0)! const j = (integer + v + p) % i const a = transformed[i] const b = transformed[j] transformed[j] = a transformed[i] = b } return transformed }Copy the code

Hashids use a salt-based selection design to make the sequence of “random” mappings in the same scenario fixed, resulting in the same encryption result.

private _encode(numbers: NumberLike[]): string[] { let { alphabet } = this const numbersIdInt = numbers.reduce<number>( (last, number, i) => last + (typeof number === 'bigint' ? Number(Number % BigInt(I + MODULO_PART)) : Number % (I + MODULO_PART)), 0,) string[] = [alphabet[numbersIdInt % alphabet.length]] const lottery = [...ret] numbers.forEach((number, i) => { const buffer = lottery.concat(this.salt, alphabet) alphabet = shuffle(alphabet, buffer) const last = toAlphabet(number, alphabet) ret.push(... last) }) return ret }Copy the code

Then, through the ** of the original data “base conversion” **, that is, to call the toAlphabet method mentioned above, and then by looking up the table, you can get the encrypted content.

Similarly, decryption method based on the result of encryption, reverse transformation can be obtained.

Private _decode(id: string): NumberLike[] {for (const subId of idArray) {const buffer = [lotteryChar,...this.salt, ...lastAlphabet] const nextAlphabet = shuffle( lastAlphabet, buffer.slice(0, lastAlphabet.length), Result. push(fromAlphabet(array. from(subId), nextAlphabet)) lastAlphabet = nextAlphabet } return result } export const fromAlphabet = ( inputChars: string[], alphabetChars: string[], ): NumberLike => inputChars.reduce<NumberLike>((carry, item) => { const index = alphabetChars.indexOf(item) const value = carry * alphabetChars.length + index const isSafeValue = Number.isSafeInteger(value) if (isSafeValue) { return value } }, 0)Copy the code

Five, the summary

The above content mainly introduces the core principles of the implementation of hashids library:

  • “To convert a longer integer number to a shorter character by a base conversion.”
  • “By modifying the Fisher-Yates shuffle algorithm based on a salt selection design, the encrypted content can be obtained randomly enough and with some security.”

Stay tuned for the next article that covers some of the details of the Hashids library. Finally, “If this article is helpful to you, please like, bookmark and share”.