Rahino Quijano
Writing — 01

Cryptography 101

Cryptographic techniques and their implementations using NodeJS

Published29.03.23Read10 minAuthorRahino Quijano
Cryptography 101

Cryptography has a set of tools for protecting your data. Each tool applies to a certain task and by combining each and every technique makes the whole infrastructure secure. This blog will cover the essential cryptographic techniques you will need to secure a file or a system and we will implement each technique using Typescript, NodeJS and crypto library.

1. Hashing

Hashing can be thought of as getting the fingerprint of a data. Exactly same data outputs the same hash but a tiny change on the data would generate a totally different hash. Hash is also a one-way function; original data cannot be extracted from the hash. This is commonly used for storing password instead of its plain text or raw data. Hashing is also used in making digital signature, more on that later.

export const hashData = (data: string, hashAlgo: string) => {
  return crypto.createHash(hashAlgo).update(data).digest('base64');
};

Data is a type of BinaryLike but for this implementation, we'll always passed stringified data. Hash algorithm determines how hash will be computed. There are several hashing algorithms like MD5 and SHA. Most popular ones are SHA-based like SHA-256, sha256 in crypto. The higher the bit, the more secure the hash is. base64 is the encoding scheme to be used for the generated hash.

2. HMAC

Hash-based Message Authentication Code or HMAC is similar to hashing but requires a secret key similar to a password before generating a hash. HMAC can be used to generate hash that cannot be recreated by other party without having the necessary secret key to generate it as plugging different secret key produces a different hash even if the data is the same.

export const HMAC = (data: string, hashAlgo: string, secret: string) => {
  return crypto.createHmac(hashAlgo, secret).update(data).digest('base64');
};

Similar to hash, we provide the data and the hashing algorithm but at the same time, a secret in the form of string is also used to generate hash.

3. Symmetric Encryption

Hash produces mixed up or scrambled data that cannot be read or decoded but what if you want to unscrambled the data and read it back again? This is where symmetric encryption comes in. Similar to HMAC, it scrambles the data based on secret key but also allows the data to be decoded using the same scrambled data and secret key. Secret key is important in this technique, two communicating party should both have exactly the same key. Communicating parties can meetup, exchange keys and so on, it depends on how they will do it. Before performing symmetric encryption, lets generate secret key and IV first.

export const generateSymmetricKey = () => {
  return crypto.randomBytes(32).toString('base64');
};

export const generateIV = () => {
  return crypto.randomBytes(16).toString('base64');
};

Quick side note, you can also use HMAC to create your own symmetric key.

After we generate symmetric key and IV, we'll create a function that allows both encryption and decryption of data.

export const symmetricAction = (
  action: 'encrypt' | 'decrypt',
  data: string,
  key: string,
  algo: string,
  iv: string
) => {
  const translatedKey = Buffer.from(key, 'base64');
  const ivBuffer = Buffer.from(iv, 'base64');

  switch (action) {
    case 'encrypt': {
      const cipher = crypto.createCipheriv(algo, translatedKey, ivBuffer);
      return cipher.update(data, 'utf8', 'base64') + cipher.final('base64');
    }
    case 'decrypt': {
      const decipher = crypto.createDecipheriv(algo, translatedKey, ivBuffer);
      return decipher.update(data, 'base64', 'utf-8') + decipher.final('utf8');
    }
  }
};

But first, why there is a need for IV aside from the secret key? Initialization vector or IV is used to randomize the encryption output which means, every time you encrypt the same data with the same key and same IV, the resulting output would be deferent than the previous, hence, strengthening the encryption because of unpredictability of the output. It should be noted that the key and IV must be the same for encryption and decryption thereby modifying our recent story that both parties should meet on the first place and exchange not only the secret key but the IV as well. For the last note, symmetric algorithm is required for performing symmetric encryption. Symmetric algorithms like RC4, AES, DES and others can be used but the latest commonly used algorithm is Advance Encryption Standard or AES and its variants. You can check the list of supported algorithms through OpenSSL.

openssl list -cipher-algorithms

4. Key Pairs

Continuing the story built from symmetric encryption, what if there is no way for you and your other party to meet and exchange secret key and IV? You might think of sending secret key and IV over a communication channel like through chat or something. But what if the communication channel you are using is unsecured and somebody is listening on it allowing them to read and steal your secret key and IV? Here comes the asymmetric encryption but before I introduce the technique, a precursor is required, key pairs.

Key pair is similar to secret key of symmetric encryption used for encryption and decryption of data but key pair consists of two different keys called private and public key. Private key should never be shared to anybody including your other party. Private key is used for decryption but can also be used for encryption. Public key can be shared to anybody i.e., your other party and the unwanted party as well. Public key can only be used for encryption but cannot be used for decryption. Key pairs are used not only for asymmetric encryption but also for creating digital signature which to be tackled later. The following snippets are functions use to create RSA-based key pair and Elliptic Curve-base key pair respectively.

export const generateRSAPair = (bitLength: number) => {
  const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: bitLength,
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
  });

  return {
    publicKey: Buffer.from(publicKey).toString('base64'),
    privateKey: Buffer.from(privateKey).toString('base64'),
  };
};
export const generateECPair = (curveName: string) => {
  const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
    namedCurve: curveName,
    publicKeyEncoding: { type: 'spki', format: 'pem' },
    privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
  });

  return {
    publicKey: Buffer.from(publicKey).toString('base64'),
    privateKey: Buffer.from(privateKey).toString('base64'),
  };
};

Key pair generated based on RSA can be used for asymmetric encryption on the next section which uses RSA as the algorithm. RSA key pair can also be used for RSA-based digital signature. Meanwhile, we will used EC-based key pair for ECDSA or elliptic curve digital signature algorithm later. EC can provide the same encryption strength as of RSA but EC can offer the same strength with shorter key length making EC faster and better compared to RSA.

Generating RSA key pair requires bit length, the higher the bit length, the stronger the encryption and the larger the payload, more on payload on next section. For EC key pair, you need to specify the name of the curve you will be using. For more curves info, you can check this blog Guidance for Choosing an Elliptic Curve Signature Algorithm in 2022.

5. Asymmetric Encryption

With our RSA key pair generated, we can now use it for encrypting and decrypting data asymmetrically. By the way, RSA stands for Rivest-Shamir-Adleman, the initials of the inventors of the algorithm.

export const asymmetricAction = (
  action: 'encrypt' | 'decrypt',
  data: string,
  key: string
) => {
  const recodedKey = Buffer.from(key, 'base64');

  switch (action) {
    case 'encrypt': {
      return crypto
        .publicEncrypt(recodedKey, Buffer.from(data))
        .toString('base64');
    }
    case 'decrypt': {
      return crypto
        .privateDecrypt(recodedKey, Buffer.from(data, 'base64'))
        .toString('utf-8');
    }
  }
};

When encrypting, stringified data is passed along with RSA public key. The function provided generates non-deterministic encrypted data similar to how symmetric encryption produces encrypted data with the use of IV, making the output unpredictable and cryptographically stronger. For decryption, stringified encrypted data and RSA private key are passed. An important note should be considered when using asymmetric encryption. This technique is not a replacement for symmetric encryption. RSA can only encrypt data with the maximum size corresponding to the RSA key pair bit length minus additional bits from padding and header data (11 bytes for PKCS#1 v1.5 padding) while symmetric encryption like AES can encrypt data without size limit RSA encryption maximum data size. In practice, asymmetric encryption is used only for exchanging small data like symmetric key, IV and so on. For example, Bob wants to communicate with Alice over unsecured channel so they would not have to meet each other. Bob generates RSA key pair then he will keep the private key but the public key will be sent to Alice. Alice and any unwanted party called middleman listening to the channel will be able to get the public key. Alice then generates symmetric key and IV and uses the public key sent by Bob to encrypt the symmetric key and IV. Alice will then send the RSA encrypted data over the unsecured channel. Bob will receive the message and again, middleman will be able to get the encrypted data but the later will not be able to decrypt the content and get the secret key because the middleman doesn't have the private key to decrypt it. Only Bob will be able to decrypt the data using private key, retrieving both symmetric key and IV. After the exchange of important data, symmetric encryption will take over therefore, solving the problem we had on our previous story of exchanging critical credentials without meeting up or communicating over unsecured channel.

6. Digital Signature

Another important technique in cryptography aside from hashing and encryption is digital signature. This cryptographic technique provides mechanism for proving the authenticity and integrity of the data received. Digital signature operates by hashing the data and performing asymmetric encryption this time using private key. Receiving end obtains the data, signature and public key. Receiver decrypts the signature using public key to get the original hash. data will be hash again by the receiver and compare it to the original hash. Data is untampered if the computed hash is the same with the original hash because any modification to the inbound data will cause the computed hash to change. Changing signature based on modified data is also not possible because only the private key of the sender can produce a signature that would match to the public key that the receiving end have. The following snippets show how to sign and verify a signature.

export const signData = (
  data: string,
  hashAlgo: string,
  privateKey: string
) => {
  const signer = crypto.createSign(hashAlgo);
  const recodedPrivateKey = Buffer.from(privateKey, 'base64');

  signer.update(data);
  return signer.sign(recodedPrivateKey, 'base64');
};

export const verifySignature = (
  data: string,
  signature: string,
  hashAlgo: string,
  publicKey: string
) => {
  const verifier = crypto.createVerify(hashAlgo);
  const recodedPublicKey = Buffer.from(publicKey, 'base64');

  verifier.update(data);
  return verifier.verify(recodedPublicKey, signature, 'base64');
};

Either EC and RSA key pair can be used on the given function. aside from data, signature, and keys, signer and verifier functions also require hash algorithm for hashing part of the digital signature. Signature generated is also non-deterministic similar to the above encryption schemes.

Conclusion

In summary, hashing algorithms are one-way cryptographic operation that produces digital fingerprint unique to a particular data. Symmetric encryption operates with a single key and an IV to generate non-deterministic encrypted data and can encrypt infinite-sized data. Asymmetric encryption is used for communicating over unsecured channel to exchange critical data but has a data size limit based no more than the bit length of RSA key pair. Lastly, digital signature is used for providing authenticity and integrity to the data.

Keep reading
Working on something like this? Let's talk