uccser/cs-field-guide

View on GitHub
csfieldguide/static/interactives/rsa-encryption/js/rsa-encryption.js

Summary

Maintainability
F
2 wks
Test Coverage
/**
 * This and rsa-decryption.js are nearly identical.
 * Ensure changes to this are made to that file also if appropriate
 */

const nodeRSA = require('node-rsa');
const constants = require('constants'); // native node.js module
// JS now supports big integers natively but not for more than a year (as at the time of writing)
const bigInt = require('big-integer');

var isPublicKey;
var isPkcs;
var isPadded;

var Key;
var Message;

var TXT_AUTOSWITCH_PKCS_PUBLIC = gettext("Detected a public PKCS key; mode and format scheme set appropriately.");
var TXT_AUTOSWITCH_PKCS_PRIVATE = gettext("Detected a private PKCS key; mode and format scheme set appropriately.");
var TXT_AUTOSWITCH_COMPONENTS_PUBLIC = gettext("Detected a public key in the components format; mode and format scheme set appropriately.");
var TXT_AUTOSWITCH_COMPONENTS_PRIVATE = gettext("Detected a private key in the components format; mode and format scheme set appropriately.");
var TXT_AUTOSWITCH_PUBLIC = gettext("Detected a public key, mode changed to public key encryption.");
var TXT_AUTOSWITCH_PRIVATE = gettext("Detected a private key, mode changed to private key encryption.");
var TXT_SUCCESS_BASE64 = gettext("Encryption successful! Result displayed in base64.");
var TXT_SUCCESS_HEX = gettext("Encryption successful! Result displayed in hexadecimal.");
var TXT_KEY_ERROR = gettext("Detected a problem with the given key, ensure it is entered exactly as it was given.");
var TXT_P_ERROR = gettext("Detected a problem with the given 'p' component, ensure it is entered exactly as it was given.");
var TXT_Q_ERROR = gettext("Detected a problem with the given 'q' component, ensure it is entered exactly as it was given.");
var TXT_D_ERROR = gettext("Detected a problem with the given 'd' component, ensure it is entered exactly as it was given.");
var TXT_ERROR_UNKNOWN = gettext("Encryption failed! Cause unidentified.");
var TXT_COPY = gettext("Copy to clipboard");
var TXT_COPIED_SUCCESS = gettext("Encrypted message copied");
var TXT_COPIED_FAIL = gettext("Oops, unable to copy. Please copy manually");

$(document).ready(function() {
  init();

  $('#rsa-encryption-key-type').change(interpretEncryptionType);
  $('#rsa-encryption-key-format').change(interpretEncryptionFormat);
  $('#rsa-encryption-key-padding').change(interpretEncryptionPadding);

  $('#rsa-encryption-components-box').on('paste', function() {
    $('#rsa-encryption-autoswitch-text').html("");
    $('#rsa-encryption-status-text').html("");
    // If we run this immediately it happens before the content is actually pasted
    setTimeout(interpretKeyComponents, 1);
  });
  $('#rsa-encryption-key').on('paste', function() {
    $('#rsa-encryption-autoswitch-text').html("");
    $('#rsa-encryption-status-text').html("");
    // If we run this immediately it happens before the content is actually pasted
    setTimeout(interpretKeyPkcs, 1);
  });

  $('#rsa-encryption-button').click(encrypt);


  $('#rsa-encryption-copy-button').click(copy);
  $('[data-toggle="tooltip"]').on('copied', function(event, message) {
    $(this).attr('title', message)
      .tooltip('_fixTitle')
      .tooltip('show')
      .attr('title', TXT_COPY)
      .tooltip('_fixTitle');
  });


  // We only want users to paste content into the components box
  // Adapted from https://stackoverflow.com/a/2904944
  var ctrlDown = false;
  var ctrlKey = 17; // Windows
  var cmdKey = 91;  // Mac
  var vKey = 86;
  $('#rsa-encryption-components-box').keydown(function(e) {
    if (e.keyCode == ctrlKey || e.keyCode == cmdKey) {
      ctrlDown = true;
    } else if (!(ctrlDown && e.keyCode == vKey)) {
      e.preventDefault();
    }
  });
  $(document).keyup(function(e) {
    if (e.keyCode == ctrlKey || e.keyCode == cmdKey) {
      ctrlDown = false;
    }
  });
});

/**
 * Prepare the interactive as it should be on page load
 */
function init() {
  Key = "";
  Message = "";

  $('#rsa-encryption-components-box').val("");
  $('#rsa-encryption-key').val("");
  $('#rsa-encryption-autoswitch-text').html("");
  $('#rsa-encryption-status-text').html("");
  $('#rsa-encryption-plaintext').val("");
  $('#rsa-encryption-ciphertext').val("");
  $('#rsa-encryption-key-e').val("");
  $('#rsa-encryption-key-n').val("");
  $('#rsa-encryption-key-p').val("");
  $('#rsa-encryption-key-q').val("");
  $('#rsa-encryption-key-d').val("");

  $('#rsa-encryption-key-type').val('public');
  interpretEncryptionType();
  // Default to PKCS (even though the generator defaults to Components).
  // This is to reduce the complexity of what the user sees initially.
  // Mode errors are fixed automatically anyway
  $('#rsa-encryption-key-format').val('pkcs');
  interpretEncryptionFormat();
  $('#rsa-encryption-key-padding').val('padding');
  interpretEncryptionPadding();
}

//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--
                            // INTERFACE  LOGIC //
//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--
// (Mostly to auto-detect and fix mode errors)

/**
 * Decides whether to use Public or Private key encryption
 */
function interpretEncryptionType() {
  var wasPublicKey = isPublicKey;
  isPublicKey = $('#rsa-encryption-key-type').val() == 'public';
  if (wasPublicKey != isPublicKey) {
    changeEncryptionType();
  }
  $('#rsa-encryption-autoswitch-text').html("");
  $('#rsa-encryption-status-text').html("");
}

/**
 * Decides whether to interpret PKCS or Components key type
 */
function interpretEncryptionFormat() {
  var wasPkcs = isPkcs;
  isPkcs = $('#rsa-encryption-key-format').val() == 'pkcs';
  if (wasPkcs != isPkcs) {
    changeEncryptionFormat();
  }
  $('#rsa-encryption-autoswitch-text').html("");
  $('#rsa-encryption-status-text').html("");
}

/**
 * Decides whether or not to add padding to the encrypted message
 */
function interpretEncryptionPadding() {
  isPadded = $('#rsa-encryption-key-padding').val() == 'padding';
}

/**
 * Called when a change in the encryption type (Public or Private key encryption) is detected.
 * Replaces the variables used in the components scheme, no visible change for PKCS scheme
 */
function changeEncryptionType() {
  $('#rsa-encryption-key-e').val("");
  $('#rsa-encryption-key-n').val("");
  $('#rsa-encryption-key-p').val("");
  $('#rsa-encryption-key-q').val("");
  $('#rsa-encryption-key-d').val("");

  if (isPublicKey) {
    $('#rsa-encryption-private-key-components').addClass('d-none');
    $('#rsa-encryption-public-key-components').removeClass('d-none');
  } else {
    $('#rsa-encryption-public-key-components').addClass('d-none');
    $('#rsa-encryption-private-key-components').removeClass('d-none');
  }
}

/**
 * Called when a change in the encryption key format type (PKCS or Components) is detected.
 * Replaces the input boxes used for entering the key
 */
function changeEncryptionFormat() {
  $('#rsa-encryption-key-e').val("");
  $('#rsa-encryption-key-n').val("");
  $('#rsa-encryption-key-p').val("");
  $('#rsa-encryption-key-q').val("");
  $('#rsa-encryption-key-d').val("");
  $('#rsa-encryption-key').val("");

  if (isPkcs) {
    $('#rsa-encryption-key-components').addClass('d-none');
    $('#rsa-encryption-key').removeClass('d-none');
  } else {
    $('#rsa-encryption-key').addClass('d-none');
    $('#rsa-encryption-key-components').removeClass('d-none');
  }
}

/**
 * Interprets what was just pasted into the key components box
 * 
 * There is no reason to run this other than on a paste event - if the user wants
 * to type it out they can just do so into the individual component boxes.
 */
function interpretKeyComponents() {
  var pastedContent = $('#rsa-encryption-components-box').val().trim();
  $('#rsa-encryption-components-box').val("");
  if (pastedContent.startsWith('--')) {
    if (pastedContent.includes('PUBLIC')) {
      // Likely pasted a PUBLIC PKCS key
      $('#rsa-encryption-key-format').val('pkcs');
      interpretEncryptionFormat();
      $('#rsa-encryption-key-type').val('public');
      interpretEncryptionType();
      $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_PKCS_PUBLIC);
    } else {
      // Likely pasted a PRIVATE PKCS key
      $('#rsa-encryption-key-format').val('pkcs');
      interpretEncryptionFormat();
      $('#rsa-encryption-key-type').val('private');
      interpretEncryptionType();
      $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_PKCS_PRIVATE);
    }
    $('#rsa-encryption-key').val(pastedContent);
  } else {
    if (isPublicKey) {
      if (pastedContent.startsWith("p:")) {
        // Likely pasted a PRIVATE key
        $('#rsa-encryption-key-type').val('private');
        interpretEncryptionType();
        $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_PRIVATE);
        $('#rsa-encryption-components-box').val(pastedContent);
        setTimeout(interpretKeyComponents, 1); // Try again
      } else {
        parsePublicKeyComponents(pastedContent);
      }
    } else /* !isPublicKey */ {
      if (pastedContent.startsWith("e:")) {
        // Likely pasted a PUBLIC key
        $('#rsa-encryption-key-type').val('public');
        interpretEncryptionType();
        $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_PUBLIC);
        $('#rsa-encryption-components-box').val(pastedContent);
        setTimeout(interpretKeyComponents, 1); // Try again
      } else {
        parsePrivateKeyComponents(pastedContent);
      }
    }
  }
}

/**
 * Splits the given text and fills the component inputs appropriately.
 */
function parsePublicKeyComponents(text) {
  var textComponents = text.split('\n');
  var keyComponents = {};
  // The component is the text after the identifier
  keyComponents.e = textComponents[textComponents.indexOf("e:") + 1];
  keyComponents.n = textComponents[textComponents.indexOf("n:") + 1];

  $('#rsa-encryption-key-e').val(keyComponents.e);
  $('#rsa-encryption-key-n').val(keyComponents.n);
}

/**
 * Splits the given text and fills the component inputs appropriately.
 */
function parsePrivateKeyComponents(text) {
  var textComponents = text.split('\n');
  var keyComponents = {};
  // The component is the text after the identifier
  keyComponents.p = textComponents[textComponents.indexOf("p:") + 1];
  keyComponents.q = textComponents[textComponents.indexOf("q:") + 1];
  keyComponents.d = textComponents[textComponents.indexOf("d:") + 1];

  $('#rsa-encryption-key-p').val(keyComponents.p);
  $('#rsa-encryption-key-q').val(keyComponents.q);
  $('#rsa-encryption-key-d').val(keyComponents.d);
}

/**
 * Checks for and resolves any mode error
 * (entered a Components key instead of PKCS, or a private/public key instead of public/private)
 */
function interpretKeyPkcs() {
  var pastedContent = $('#rsa-encryption-key').val().trim();
  if (pastedContent.startsWith("e:")) {
    // Likely pasted a PUBLIC Components key
    $('#rsa-encryption-key-format').val('components');
    interpretEncryptionFormat();
    $('#rsa-encryption-key-type').val('public');
    interpretEncryptionType();
    $('#rsa-encryption-components-box').val(pastedContent);
    interpretKeyComponents();
    $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_COMPONENTS_PUBLIC);
    $('#rsa-encryption-key').val("");
  } else if (pastedContent.startsWith("p:")) {
    // Likely pasted a PRIVATE Components key
    $('#rsa-encryption-key-format').val('components');
    interpretEncryptionFormat();
    $('#rsa-encryption-key-type').val('private');
    interpretEncryptionType();
    $('#rsa-encryption-components-box').val(pastedContent);
    interpretKeyComponents();
    $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_COMPONENTS_PRIVATE);
    $('#rsa-encryption-key').val("");
  } else if (!isPublicKey && pastedContent.includes("PUBLIC")) {
    // Likely pasted a PUBLIC key
    $('#rsa-encryption-key-type').val('public');
    interpretEncryptionType();
    $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_PUBLIC);
  } else if (isPublicKey && pastedContent.includes("PRIVATE")) {
    // Likely pasted a PRIVATE key
    $('#rsa-encryption-key-type').val('private');
    interpretEncryptionType();
    $('#rsa-encryption-autoswitch-text').html(TXT_AUTOSWITCH_PRIVATE);
  }
}

//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--
                            // ENCRYPTION LOGIC //
//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--

/**
 * Takes the text in the input box, encrypts it with the key and prints it to the output box
 */
function encrypt() {
  $('#rsa-encryption-status-text').html("");
  $('#rsa-encryption-autoswitch-text').html("");
  if (isPkcs) {
    // If the user typed in the key then the mode-error checking hasn't been done, so repeat just in case.
    interpretKeyPkcs();
  }
  // The previous statement could have changed this value so check again
  if (isPkcs) {
    try {
      Key = new nodeRSA($('#rsa-encryption-key').val().trim());
    } catch (error) {
      $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_KEY_ERROR + '</span>');
      return;
    }
  } else {
    Key = new nodeRSA();
    var components = {};
    if (isPublicKey) {
      components.e = parseInt($('#rsa-encryption-key-e').val().trim().split(' ').join('').toLowerCase(), 16),
      components.n = $('#rsa-encryption-key-n').val().trim().split(' ').join('').toLowerCase();
      try {
        Key.importKey({
          n: Buffer.from(components.n, 'hex'),
          e: components.e
        }, 'components-public');
      } catch (error) {
        $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_KEY_ERROR + '</span>');
        return;
      }
    } else {
      try {
        components = getPrivateComponents();
      } catch (error) {
        if (error == "P_ERROR") {
          $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_P_ERROR + '</span>');
        } else if (error == "Q_ERROR") {
          $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_Q_ERROR + '</span>');
        } else if (error == "D_ERROR") {
          $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_D_ERROR + '</span>');
        } else {
          $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_ERROR_UNKNOWN + '</span>');
          console.log(error); // If the user is tech-savvy enough maybe they can see what's wrong themselves
        }
        return;
      }
      try {
        Key.importKey(components, 'components');
      } catch (error) {
        $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_KEY_ERROR + '</span>');
        return;
      }
    }
  }
  Message = $('#rsa-encryption-plaintext').val();
  if (isPadded) {
    Key.setOptions({
      encryptionScheme: 'pkcs1'
    });
  } else {
    Key.setOptions({
      encryptionScheme: {
        scheme: 'pkcs1',
        padding: constants.RSA_NO_PADDING
      }
    });
  }
  libraryEncrypt();

  $('#rsa-encryption-copy-button').removeClass('disabled').prop('disabled', false);

  $('[data-toggle="tooltip"]').tooltip();
}

/**
 * Returns a dictionary of the 8 required components to form a private key in our encryption library, in the format required by it:
 * [n, e, d, p, q, dmp1, dmq1, coeff]
 * d, p & q are entered by the user, e is left as default, the rest can be calculated.
 * Unsure if these calculated values get used at all during the process, as only d, p & q are required for encryption
 */
function getPrivateComponents() {
  // Data has had a lot of changes: Buffer(?) as string -> bigInt -> calculations -> string -> hex -> Buffer
  // There is no perceivable delay but the efficiency could be investigated further in future

  var defaultE = 65537; // TODO: calculate e also (we can't assume this is true for people who don't use the key generator)

  // User entered values
  var strP = $('#rsa-encryption-key-p').val().trim().split(' ').join('').toLowerCase();
  var strQ = $('#rsa-encryption-key-q').val().trim().split(' ').join('').toLowerCase();
  var strD = $('#rsa-encryption-key-d').val().trim().split(' ').join('').toLowerCase();

  // Error control
  try {
    var intP = new bigInt(strP, 16);
  } catch (error) {
    throw "P_ERROR";
  }
  try {
    var intQ = new bigInt(strQ, 16);
  } catch (error) {
    throw "Q_ERROR";
  }
  try {
    var intD = new bigInt(strD, 16);
  } catch (error) {
    throw "D_ERROR";
  }

  // Calculate remaining values
  var intN = intP.times(intQ);
  var intDmp1 = intD.mod(intP - 1);
  var intDmq1 = intD.mod(intQ - 1);
  var intCoeff = modInverse(intQ, intP);

  // Format appropriately for use
  components = {
    n: Buffer.from(intN.toString(16), 'hex'),
    e: defaultE,
    d: Buffer.from(strD, 'hex'),
    p: Buffer.from(strP, 'hex'),
    q: Buffer.from(strQ, 'hex'),
    dmp1: Buffer.from(intDmp1.toString(16), 'hex'),
    dmq1: Buffer.from(intDmq1.toString(16), 'hex'),
    coeff: Buffer.from(intCoeff.toString(16), 'hex')
  }

  return components;
}

/**
 * Encrypts the message using the included JS library, with appropriate padding.
 * Padding scheme is pkcs1 type (library limitation) or none - as defined by the user
 */
function libraryEncrypt() {
  var encryptedData;
  try {
    if (isPkcs) {
      if (isPublicKey) {
        encryptedData = Key.encrypt(Message, 'base64');
      } else {
        encryptedData = Key.encryptPrivate(Message, 'base64');
      }
      $('#rsa-encryption-ciphertext').val(encryptedData);
      $('#rsa-encryption-status-text').html('<span class="text-success">' + TXT_SUCCESS_BASE64 + '</span>');
    } else {
      if (isPublicKey) {
        encryptedData = Key.encrypt(Message, 'hex');
      } else {
        encryptedData = Key.encryptPrivate(Message, 'hex');
      }
      $('#rsa-encryption-ciphertext').val(encryptedData.toUpperCase());
      $('#rsa-encryption-status-text').html('<span class="text-success">' + TXT_SUCCESS_HEX + '</span>');
    }
  } catch (error) {
    $('#rsa-encryption-status-text').html('<span class="text-danger">' + TXT_ERROR_UNKNOWN + '</span>');
    console.log(error); // If the user is tech-savvy enough maybe they can see what's wrong themselves
    return;
  }
}

/**
 * Copies the ciphertext to user's clipboard
 */
function copy() {
  $('#rsa-encryption-ciphertext').select();
  try {
    var successful = document.execCommand('copy');
    if (successful) {
      $('#rsa-encryption-copy-button').trigger('copied', TXT_COPIED_SUCCESS);
    } else {
      $('#rsa-encryption-copy-button').trigger('copied', TXT_COPIED_FAIL);
    }
  } catch (err) {
    $('#rsa-encryption-copy-button').trigger('copied', TXT_COPIED_FAIL);
  }
}

//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--
                            //   OTHER  LOGIC   //
//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--//--
// (Functions copied from a third party)

/**
 * Calculates the modular multiplicative inverse of two values.
 * Copied directly from Dipu on stackoverflow: https://stackoverflow.com/a/51562038
 */
function modInverse(a, m) {
  // validate inputs
  [a, m] = [Number(a), Number(m)]
  if (Number.isNaN(a) || Number.isNaN(m)) {
    return NaN // invalid input
  }
  a = (a % m + m) % m
  if (!a || m < 2) {
    return NaN // invalid input
  }
  // find the gcd
  const s = []
  let b = m
  while(b) {
    [a, b] = [b, a % b]
    s.push({a, b})
  }
  if (a !== 1) {
    return NaN // inverse does not exist
  }
  // find the inverse
  let x = 1
  let y = 0
  for(let i = s.length - 2; i >= 0; --i) {
    [x, y] = [y,  x - y * Math.floor(s[i].a / s[i].b)]
  }
  return (y % m + m) % m
}