Support Ukraine πŸ‡ΊπŸ‡¦Help Ukrainian ArmyHumanitarian Assistance to Ukrainians

A simple implementation of the RSA algorithm in JavaScript

Alice

Oct 05 2020 at 20:30 GMT

In the RSA cryptosystem, the receiver of messages has a public key, which is publicly shared, and a secret key, which is kept private.

The sender of a message uses the public key to encrypt the message. Then, the receiver is able to decrypt the message using the private key.

Could someone explain how this works in more detail and provide a simple JavaScript implementation?

1 Answer

Tom

Oct 05 2020 at 22:13 GMT

Let's go through the three phases of RSA: key generation, encryption, and decryption.

I will also show a JavaScript implementation of each step.

Note: since we will be working with very large numbers, we will be using the new JavaScript BigInt type instead of the simple number type. This means that all integers will be written with an n suffix, e.g., 191n.

Key generation

Let's first see how the receiver creates a public key and a secret key.

First, two distinct primes numbers pp and qq are chosen. These are generally very large, but for the purposes of this explanation, let's keep them small.

const p = 191n;
const q = 223n;

Then, we compute the integer nn as the product of pp and qq.

const n = p * q;
// 42593n

Next, we compute the value of the Euler's totient function for nn, denoted as Ο•(n)\phi(n), which represents the number of integers that are relatively prime to nn in the range 1 to nn.

Since nn is equal to the product of two distinct primes, it can be proven that:

Ο•(n)=(pβˆ’1)(qβˆ’1)\phi(n) = (p - 1)(q - 1)

Let's refer to this value using the phi variable.

const phi = (p - 1n) * (q - 1n);
// 42180n

Next, we choose the encryption exponent ee, such that 1<e<Ο•(n)1 < e < \phi(n) and ee is relatively prime to Ο•(n)\phi(n), i.e., the gcd⁑(e,Ο•(n))=1\gcd(e, \phi(n)) = 1.

We can pick a prime number for ee and then check that it does not divide Ο•(n)\phi(n). The number 47 is prime and does not divide 42180, so let's go with e=47e=47.

const e = 47n;

More generally, we can write a generateEncryptionExponent function that generates ee by starting with 47 and checking if it is relatively prime to Ο•(n)\phi(n) and incrementing it by 2 if it is not relatively prime until we get a number that is relatively prime to Ο•(n)\phi(n):

function generateEncryptionExponent(phi) {
  let e = 47n;

  while (gcd(e, phi) !== 1n) {
    e += 2n;
  }

  return e;
}

Now, let's compute the decryption exponent dd, which is the multiplicative inverse of ee modulo Ο•(n)\phi(n). That is,

ed≑1(modΟ•(n))ed \equiv 1 \pmod{\phi(n)}

It can be proven that dd is the coefficient of ee in the linear combination of ee and Ο•(n)\phi(n) that expresses the gcd⁑(e,Ο•(n))\gcd(e, \phi(n)), which we can compute using the Extended Euclidean Algorithm.

So, let's assume that we have an extendedGcd function that implements the Extended Euclidean Algorithm.

This function takes two integers a and b for which we want to compute the GCD, and it returns an object that has the value of the GCD as well as the coefficients s and t in the linear combination of a and b that is equal to the GCD. That is,

gcd⁑(a,b)=sa+tb  for some s,t\gcd(a, b) = sa + tb \ \text{ for some } s,t

So, if we compute the GCD of ee and Ο•(n)\phi(n), dd will correspond to the coefficient of ee, which will be the first one, i.e., ss.

Furthermore, we want the decryption exponent dd to be positive. So, if we get a negative value for dd, we can make it positive by adding Ο•(n)\phi(n) to it as many times as needed to make it positive.

Here's the computeDecryptionExponent function which computes dd:

function computeDecryptionExponent(e, phi) {
  let d = extendedGcd(e, phi).s;

  while (d < 1n) {
    d += phi;
  }

  return d;
}

The public key will be a pair that consists of the encryption exponent ee and the integer nn.

const publicKey = { e, n };

The secret key will be a pair that consists of the decryption exponent dd and the integer nn.

const secretKey = { d, n };

Encryption

When the sender wants to encrypt a message, the message first needs to be converted to an integer.

Let's say that the resulting message expressed as an integer is mm. It is required that:

0≀m<n0 \le m < n

The sender should also check that gcd⁑(m,n)=1\gcd(m, n) = 1, since we don't want the GCD to equal pp or qq, which in practice almost never happens.

Next, the message mm is encrypted into the ciphertext cc using the public key by computing the remainder of mem^e divided by nn.

c=rem(me,n)c = \text{rem}(m^e, n)

Here's the encrypt function that does the encryption:

function encrypt(m, publicKey) {
  const { e, n } = publicKey;

  if (m < 0n || m >= n) {
    throw new Error(`Condition 0 <= m < n not met. m = ${m}`);
  }

  if (gcd(m, n) !== 1n) {
    throw new Error("Condition gcd(m, n) = 1 not met.");
  }

  const c = m ** e % n;

  return c;
}

Decryption

The receiver can decrypt the ciphertext cc back to the original message mm using the secret key by computing the remainder of cdc^d divided by nn.

m=rem(cd,n)m = \text{rem}(c^d, n)

Here's the decrypt function that does the decryption:

function decrypt(c, secretKey) {
  const { d, n } = secretKey;

  const m = c ** d % n;

  return m;
}

Finally, here's the rsaExample function that puts all of this together:

function rsaExample() {
  const p = 191n;
  const q = 223n;

  const n = p * q;
  const phi = (p - 1n) * (q - 1n);

  const e = generateEncryptionExponent(phi);
  const d = computeDecryptionExponent(e, phi);

  const publicKey = { e, n };
  const secretKey = { d, n };

  const m = textToNumber("Hi");
  const c = encrypt(m, publicKey);
  const m2 = decrypt(c, secretKey);

  console.log(numberToText(m2));
  // Hi
}

Note that this is just a simple JavaScript implementation of the RSA algorithm shown for learning purposes.

If we try it with big prime numbers and long messages, we will end up with extremely large numbers that cannot even fit inside the JavaScript BigInt, and so we will get the RangeError: Maximum BigInt size exceeded error.

claritician Β© 2022