Skip to content

Instantly share code, notes, and snippets.

@santoshshinde2012
Last active November 29, 2023 18:17
Show Gist options
  • Save santoshshinde2012/1913bcb3d92b35b042ddcf28e0543413 to your computer and use it in GitHub Desktop.
Save santoshshinde2012/1913bcb3d92b35b042ddcf28e0543413 to your computer and use it in GitHub Desktop.
Secure Client-Side Storage Data Using With Web Crypto (Typescript, AES-GCM, PBKDF2)
export default class Crypto {
    // iterations: It must be a number and should be set as high as possible.
    // So, the more is the number of iterations, the more secure the derived key will be,
    // but in that case it takes greater amount of time to complete.
    // number of interation - the value of 2145 is randomly chosen
    private static iteration = 10;
  
    // algorithm - AES 256 GCM Mode
    private static encryptionAlgorithm = "AES-GCM";
  
    // random initialization vector length
    private static ivLength = 12;
  
    // random salt length
    private static saltLength = 16;
  
    // digest: It is a digest algorithms of string type.
    private static digest = "SHA-256";
  
    // text encoder
    private static enc = new TextEncoder();
  
    // text decoder
    private static dec = new TextDecoder();
  
    /**
     *
     * @param u8
     * @returns
     */
    private static base64Encode(u8: Uint8Array): string {
      return btoa(String.fromCharCode.apply(undefined, [...u8]));
    }
  
    /**
     *
     * @param str
     * @returns
     */
    private static base64Decode(str: string): Uint8Array {
      return Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
    }
  
    /**
     *
     * @param secretKey
     * @returns
     */
    private static getPasswordKey(secretKey: string): Promise<CryptoKey> {
      return window.crypto.subtle.importKey(
        "raw",
        Crypto.enc.encode(secretKey),
        "PBKDF2",
        false,
        ["deriveKey"]
      );
    }
  
    /**
     *
     * @param passwordKey
     * @param salt
     * @param iteration
     * @param digest
     * @param encryptionAlgorithm
     * @param keyUsage
     * @returns
     */
    private static deriveKey(
      passwordKey: CryptoKey,
      salt: Uint8Array,
      iteration: number,
      digest: string,
      encryptionAlgorithm: string,
      keyUsage: ["encrypt"] | ["decrypt"]
    ): Promise<CryptoKey> {
      return window.crypto.subtle.deriveKey(
        {
          name: "PBKDF2",
          salt,
          iterations: iteration,
          hash: digest,
        },
        passwordKey,
        {
          name: encryptionAlgorithm,
          length: 256,
        },
        false,
        keyUsage
      );
    }
  
    /**
     *
     * @param secretKey
     * @param data
     * @returns
     */
    public static async encrypt(
      secretKey: string,
      data: string
    ): Promise<string> {
      try {
        // generate random salt
        const salt = window.crypto.getRandomValues(
          new Uint8Array(Crypto.saltLength)
        );
  
        // How to transport IV ?
        // Generally the IV is prefixed to the ciphertext or calculated using some kind of nonce on both sides.
        const iv = window.crypto.getRandomValues(new Uint8Array(Crypto.ivLength));
  
        // create master key from secretKey
        // The method gives an asynchronous Password-Based Key Derivation
        // Create a password based key (PBKDF2) that will be used to derive the AES-GCM key used for encryption
        const passwordKey = await Crypto.getPasswordKey(secretKey);
  
        // to derive a secret key from a master key for encryption
        // Create an AES-GCM key using the PBKDF2 key and a randomized salt value.
        const aesKey = await Crypto.deriveKey(
          passwordKey,
          salt,
          Crypto.iteration,
          Crypto.digest,
          Crypto.encryptionAlgorithm,
          ["encrypt"]
        );
  
        // create a Cipher object, with the stated algorithm, key and initialization vector (iv).
        // @algorithm - AES 256 GCM Mode
        // @key
        // @iv
        // @options
        // Encrypt the input data using the AES-GCM key and a randomized initialization vector (iv).
        const encryptedContent = await window.crypto.subtle.encrypt(
          {
            name: Crypto.encryptionAlgorithm,
            iv,
          },
          aesKey,
          Crypto.enc.encode(data)
        );
  
        // convert encrypted string to buffer
        const encryptedContentArr: Uint8Array = new Uint8Array(encryptedContent);
  
        // create buffer array with length [salt + iv + encryptedContentArr]
        const buff: Uint8Array = new Uint8Array(
          salt.byteLength + iv.byteLength + encryptedContentArr.byteLength
        );
  
        // set salt at first postion
        buff.set(salt, 0);
  
        // set iv at second postion
        buff.set(iv, salt.byteLength);
        // set encrypted at third postion
        buff.set(encryptedContentArr, salt.byteLength + iv.byteLength);
        // encode the buffer array
        const base64Buff: string = Crypto.base64Encode(buff);
  
        // return encrypted string
        return base64Buff;
      } catch (error) {
        // if any expection occurs
        console.error(`Error - ${error}`);
        return "";
      }
    }
  
    /**
     *
     * @param secretKey
     * @param ciphertext
     * @returns
     */
    public static async decrypt(secretKey: string, ciphertext: string) {
      try {
        // Creates a new Buffer containing the given JavaScript string {str}
        const encryptedDataBuff = Crypto.base64Decode(ciphertext);
  
        // extract salt from encrypted data
        const salt = encryptedDataBuff.slice(0, Crypto.saltLength);
  
        // extract iv from encrypted data
        const iv = encryptedDataBuff.slice(
          Crypto.saltLength,
          Crypto.saltLength + Crypto.ivLength
        );
  
        // extract encrypted text from encrypted data
        const data = encryptedDataBuff.slice(Crypto.saltLength + Crypto.ivLength);
  
        // create master key from secretKey
        // The method gives an asynchronous Password-Based Key Derivation
        // Create a password based key (PBKDF2) that will be used to derive the AES-GCM key used for decryption.
        const passwordKey = await Crypto.getPasswordKey(secretKey);
  
        // to derive a secret key from a master key for decryption
        // Create an AES-GCM key using the PBKDF2 key and the salt from the ArrayBuffer.
        const aesKey = await Crypto.deriveKey(
          passwordKey,
          salt,
          Crypto.iteration,
          Crypto.digest,
          Crypto.encryptionAlgorithm,
          ["decrypt"]
        );
  
        // Return the buffer containing the value of cipher object.
        // Decrypt the input data using the AES-GCM key and the iv from the ArrayBuffer.
        const decryptedContent = await window.crypto.subtle.decrypt(
          {
            name: Crypto.encryptionAlgorithm,
            iv,
          },
          aesKey,
          data
        );
  
        // Returns the result of running encoding's decoder.
        return Crypto.dec.decode(decryptedContent);
      } catch (error) {
        // if any expection occurs
        console.error(`Error - ${error}`);
        return "";
      }
    }
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment