Steve Davis
Time-based Data Masking with Vue.js
Two people collaboarting on work

Data security is a vital part of many of our projects at Bio::Neos. For one of our projects specifically, that data is in the form of patient information. For handling these kinds of projects, not only must we ensure that we are developing HIPAA-compliant software and infrastructure, but we also are required to add safeguards against that data being leaked due to user error. One solution we’ve come up with is to introduce a “masking timer” that could be used to hide any sensitive data being displayed on the screen after a set period of time while at the same time allowing the user to hide/unhide the data as they see fit. For example, this is useful for cases where the user might forget to log out from the application before getting up and leaving their computer unattended; At the same time, we implement idle-based automatic logouts. This won’t protect data in the short-term until that logout timer is hit. In this post, I’ll describe the steps we took to develop our timer switch for data masking.

Writing the Masking Switch/Timer Component

The front end of this project is written in Vue.js (version 2), so we first started by creating a generic-looking component that could run a timer and then alert us when that timer ran out.

<template>
  <div></div>
</template>


<script>
const MASKING_TIMEOUT_MILLIS = 5 * 60 * 1000; // Default 5 mins
let maskingTimeout = null;
export default {
  mounted() {
    this.startMaskingTimer();
  },
  beforeDestroy() {
    this.cancelMaskingTimer();
  },
  methods: {
    startMaskingTimer()
    {
      maskingTimeout = setTimeout(() => {
        this.alertMaskingTimerExpired();
      }, MASKING_TIMEOUT_MILLIS);
    },
    cancelMaskingTimer()
    {
      if (maskingTimeout) 
        clearTimeout(maskingTimeout);
    },
    alertMaskingTimerExpired()
    {
      this.$emit('timerExpired');
    }
  }
}
</script>

Here we have a Vue.js component that starts a timer when mounted and emits an event named timerExpired when the time runs out. When the timer expires, our parent component can listen for this event and act accordingly (in our case, mask any data that needs to be hidden). But we don’t want the timer to run out while the user is actively using the app. To accommodate, let’s listen for any events that might indicate that the user is still interacting with our application and restart the timer if that’s the case:

<template>
  <div></div>
</template>


<script>
const MASKING_TIMEOUT_MILLIS = 5 * 60 * 1000; // Default 5 mins
const EVENTS = ['click', 'keypress'];
let maskingTimeout = null;
export default {
  mounted() {
    this.startMaskingTimer();
  },
  beforeDestroy() {
    this.cancelMaskingTimer();
    this.removeActivityEventListeners();
  },
  methods: {
    startMaskingTimer()
    {
      maskingTimeout = setTimeout(() => {
        this.alertMaskingTimerExpired();
      }, MASKING_TIMEOUT_MILLIS);
      EVENTS.forEach(event => {
        window.addEventListener(event, this.onActivityDetected, true);
      });
    },
    cancelMaskingTimer()
    {
      if (maskingTimeout) 
        clearTimeout(maskingTimeout);
    },
    alertMaskingTimerExpired()
    {
      this.$emit('timerExpired');
    },
    onActivityDetected() {
      // Restart the timer
      this.cancelMaskingTimer();
      this.startMaskingTimer();
    },
    removeActivityEventListeners() {
      // Remove event listeners
      EVENTS.forEach(event => {
        window.removeEventListener(event, this.onActivityDetected, true);
      });
    },
  }
}
</script>

Here we’ve added a couple of new methods: onActivityDetected() and removeActivityEventListeners(). When the masking timer starts, we add event listeners to the window that listen for whatever we deem user activity. In this example, we are listening for any clicks or key presses. If any user activity is detected, then we call onActivityDetected() and restart the masking timer.

This is great; however, what if the masking timer runs out, so the data is hidden, but the user returns to their computer and wants to see it again? Let’s hook up this timer to a UI component that lets the user unmask or mask the data on their own:

 <template>
   <b-switch
    v-model="isDataMasked"
    left-label>
    Privacy Switch
   </b-switch>
 </template>
...
... 
},
  data() {
    return {
      isDataMasked: true
    }
  }
...

Now we’ve added a switch component (courtesy of a components framework called buefy (https://buefy.org) to the template part of our Vue component. Whether or not the switch is “on” is determined by a reactive variable we’re calling isDataMasked.

export default {
  mounted() {
    // Emit the current state of the mask data switch
    this.$emit('switch', this.isDataMasked);
    if (!this.isDataMasked)
    {
      this.startMaskingTimer();
    }
  },
  beforeDestroy() {
    this.cancelMaskingTimer();
    this.removeActivityEventListeners();
  },
  watch: {
    isDataMasked(newVal, oldVal) {
      // Broadcast change in the mask data switch
      this.$emit('switch', newVal);
      if (!newVal)
      {
        this.startMaskingTimer();
      }
      else
      {
        // Cancel the masking timer if the switch is toggled on (indicating data is already masked)
        this.cancelMaskingTimerAndInterval();
        this.removeActivityEventListeners();
      }
    }
  },
  methods: {
    startMaskingTimer()
    {
      maskingTimeout = setTimeout(() => {
        this.alertMaskingTimerExpired();
      }, MASKING_TIMEOUT_MILLIS);
      EVENTS.forEach(event => {
        window.addEventListener(event, this.onActivityDetected, true);
      });
    },
    cancelMaskingTimer()
    {
      if (maskingTimeout) 
        clearTimeout(maskingTimeout);
    },
    alertMaskingTimerExpired()
    {
      this.isDataMasked = true;
    },
    onActivityDetected() {
      // Restart the timer
      this.cancelMaskingTimer();
      this.startMaskingTimer();
    },
    removeActivityEventListeners() {
      // Remove event listeners
      EVENTS.forEach(event => {
        window.removeEventListener(event, this.onActivityDetected, true);
      });
    },
  },
...

In the script part of our component, we needed to add a few things:

1. When the component is mounted, we want to emit what state the switch is in so that right away, the parent component knows what it needs to do

2. We are no longer emitting the timerExpired event and are instead changing the isDataMasked variable to true whenever the timer expires.

3. Probably the most important change to recognize here is that we’ve added a watch on the isDataMasked variable.

When isDataMasked is set to true by either the user flipping the switch or the timer expiring, we want to emit the state of the switch back to the parent component and then cancel the timer and remove our event listeners. There’s no use in keeping the timer running while the data is currently being masked. On the other hand, if isDataMasked is set to false by the user flipping off the switch, we want to emit the state of the switch back to the parent component and then start up the timer again.

Hooking up the Masking Switch/Timer Component to the Parent

So, now that we’ve written our masking timer component, how do we actually use it? All that needs to be done from here is to import it into our parent component and add an event handler for the switch event:

<template>
  <div>
    <MaskDataSwitch v-on:switch="toggleDataMasking"</MaskDataSwitch>
    <p v-if="masked">##########</p>
    <p v-else>Sensitive Patient Data!</p>
  </div>
</template>


<script>
import './components/MaskDataSwitch';
export default {
  components: { MaskDataSwitch },
  methods: {
    toggleDataMasking(switchValue)
    {
      this.masked = switchValue;
    }
  },
  data() {
    return {
      masked: true
    }
  }
}
</script>
...

And that’s it! Using this implementation, we created a modular and reusable component involving a timer and a switch that could handle data masking across our app. And because the masking component is completely agnostic to the content that it is actually masking, we can use it anywhere we have privacy concerns!

Thanks for reading the Bio::Neos blog! We certainly hope this post is both insightful and engaging. Be sure to contact us with questions and comments and follow us on LinkedIn for more updates.