From 8253d2810267dbfd53e1c08c58407a57c0841685 Mon Sep 17 00:00:00 2001
From: Bubka <858858+Bubka@users.noreply.github.com>
Date: Mon, 16 Nov 2020 09:38:28 +0100
Subject: [PATCH] Fix #11 : Set TOTP loop duration on remaining time instead of
remaining dots
---
app/TwoFAccount.php | 56 +++++---------
resources/js/components/TokenDisplayer.vue | 85 ++++++++++++++++------
resources/sass/app.scss | 16 ++--
3 files changed, 84 insertions(+), 73 deletions(-)
diff --git a/app/TwoFAccount.php b/app/TwoFAccount.php
index c617b45a..b995b097 100644
--- a/app/TwoFAccount.php
+++ b/app/TwoFAccount.php
@@ -40,7 +40,7 @@ class TwoFAccount extends Model implements Sortable
*
* @var array
*/
- protected $appends = ['token', 'isConsistent', 'otpType', 'secret', 'algorithm', 'digits', 'totpPeriod', 'totpPosition', 'hotpCounter', 'imageLink'];
+ protected $appends = ['token', 'isConsistent', 'otpType', 'secret', 'algorithm', 'digits', 'totpPeriod', 'totpTimestamp', 'hotpCounter', 'imageLink'];
/**
@@ -56,7 +56,7 @@ class TwoFAccount extends Model implements Sortable
*
* @var OTPHP/TOTP || OTPHP/HOTP
*/
- protected $otp;
+ protected $otp, $timestamp;
/**
@@ -348,29 +348,6 @@ class TwoFAccount extends Model implements Sortable
}
- /**
- * Calculate where is now() in the totp current period
- * @return mixed The position
- */
- private function getTotpPosition()
- {
- // For memo :
- // $nextOtpAt = ($PeriodCount+1)*$period
- // $remainingTime = $nextOtpAt - time()
- if( $this->otpType === 'totp' ) {
-
- $currentPosition = time();
- $PeriodCount = floor($currentPosition / $this->totpPeriod); //nombre de période de x s depuis T0 (x=30 par défaut)
- $currentPeriodStartAt = $PeriodCount * $this->totpPeriod;
- $positionInCurrentPeriod = $currentPosition - $currentPeriodStartAt;
-
- return $positionInCurrentPeriod;
- }
-
- return null;
- }
-
-
/**
* Update the uri attribute using the OTP object
* @return void
@@ -382,12 +359,15 @@ class TwoFAccount extends Model implements Sortable
/**
- * Generate a token which is valid at the current time (now)
+ * Generate a token which is valid at the current time
* @return string The generated token
*/
public function generateToken() : string
{
- return $this->otpType === 'totp' ? $this->otp->now() : $this->otp->at($this->otp->getCounter());
+ $this->timestamp = time();
+ $token = $this->otpType === 'totp' ? $this->otp->at($this->timestamp) : $this->otp->at($this->otp->getCounter());
+
+ return $token;
}
@@ -405,6 +385,17 @@ class TwoFAccount extends Model implements Sortable
}
+ /**
+ * get totpTimestamp attribute
+ *
+ * @return int The timestamp
+ */
+ public function getTotpTimestampAttribute()
+ {
+ return $this->timestamp;
+ }
+
+
/**
* get token attribute
*
@@ -416,17 +407,6 @@ class TwoFAccount extends Model implements Sortable
}
- /**
- * get totpPosition attribute
- *
- * @return int The position
- */
- public function getTotpPositionAttribute()
- {
- return $this->getTotpPosition();
- }
-
-
/**
* get OTP Type attribute
*
diff --git a/resources/js/components/TokenDisplayer.vue b/resources/js/components/TokenDisplayer.vue
index 83640f45..d4f78303 100644
--- a/resources/js/components/TokenDisplayer.vue
+++ b/resources/js/components/TokenDisplayer.vue
@@ -7,7 +7,7 @@
{{ internal_account }}
{{ displayedToken }}
- counter: {{ internal_hotpCounter }}
@@ -23,13 +23,18 @@
return {
id: null,
token : '',
- timerID: null,
+ remainingTimeout: null,
+ firstDotToNextOneTimeout: null,
+ dotToDotInterval: null,
position: null,
+ totpTimestamp: null,
internal_otpType: '',
internal_account: '',
internal_service: '',
internal_icon: '',
internal_hotpCounter: null,
+ lastActiveDot: null,
+ dotToDotCounter: null,
}
},
@@ -121,42 +126,62 @@
getTOTP: function() {
+ this.dotToDotCounter = 0
+
this.axios.post('/api/twofaccounts/otp', { id: this.id, otp: this.$props }).then(response => {
+
let spacePosition = Math.ceil(response.data.token.length / 2);
this.token = response.data.token.substr(0, spacePosition) + " " + response.data.token.substr(spacePosition);
- this.position = response.data.totpPosition;
+ this.totpTimestamp = response.data.totpTimestamp; // the timestamp used to generate the token
+ this.position = this.totpTimestamp % response.data.totpPeriod // The position of the totp timestamp in the current period
+ // Hide all dots
let dots = this.$el.querySelector('.dots');
- // clear active dots
while (dots.querySelector('[data-is-active]')) {
dots.querySelector('[data-is-active]').removeAttribute('data-is-active');
}
- // set dot at given position as the active one
- let active = dots.querySelector('li:nth-child(' + (this.position + 1 ) + ')');
- active.setAttribute('data-is-active', true);
+ // Activate the dot at the totp position
+ let relativePosition = (this.position * 10) / response.data.totpPeriod
+ let dotNumber = (Math.floor(relativePosition) +1)
- let self = this;
+ this.lastActiveDot = dots.querySelector('li:nth-child(' + dotNumber + ')');
+ this.lastActiveDot.setAttribute('data-is-active', true);
- this.timerID = setInterval(function() {
+ // Main timeout which run all over the totpPeriod.
- let sibling = active.nextSibling;
+ let remainingTimeBeforeEndOfPeriod = response.data.totpPeriod - this.position
+ let self = this; // because of the setInterval/setTimeout closures
- if(active.nextSibling === null) {
- console.log('no more sibling to activate, we refresh the OTP')
- self.stopLoop()
- self.getTOTP();
- }
- else
- {
- active.removeAttribute('data-is-active');
- sibling.setAttribute('data-is-active', true);
- active = sibling
- }
+ this.remainingTimeout = setTimeout(function() {
+ self.stopLoop()
+ self.getTOTP();
+ }, remainingTimeBeforeEndOfPeriod*1000);
- }, 1000);
+ // During the remainingTimeout countdown we have to show a next dot every durationBetweenTwoDots seconds
+ // except for the first next dot
+
+ let durationBetweenTwoDots = response.data.totpPeriod / 10 // we have 10 dots
+ let firstDotTimeout = (Math.ceil(this.position / durationBetweenTwoDots) * durationBetweenTwoDots) - this.position
+
+ this.firstDotToNextOneTimeout = setTimeout(function() {
+
+ if( firstDotTimeout > 0 ) {
+ self.activeNextDot()
+ dotNumber += 1
+ }
+
+ self.dotToDotInterval = setInterval(function() {
+
+ self.dotToDotCounter += 1
+ self.activeNextDot()
+ dotNumber += 1
+
+ }, durationBetweenTwoDots*1000)
+
+ }, firstDotTimeout*1000)
})
.catch(error => {
this.$router.push({ name: 'genericError', params: { err: error.response } });
@@ -183,7 +208,7 @@
clearOTP: function() {
this.stopLoop()
- this.id = this.timerID = this.position = this.internal_hotpCounter = null
+ this.id = this.remainingTimeout = this.dotToDotInterval = this.firstDotToNextOneTimeout = this.position = this.internal_hotpCounter = null
this.internal_service = this.internal_account = this.internal_icon = this.internal_otpType = ''
this.token = '... ...'
@@ -199,7 +224,19 @@
stopLoop: function() {
if( this.internal_otpType === 'totp' ) {
- clearInterval(this.timerID)
+ clearTimeout(this.remainingTimeout)
+ clearTimeout(this.firstDotToNextOneTimeout)
+ clearInterval(this.dotToDotInterval)
+ }
+ },
+
+
+ activeNextDot: function() {
+ if(this.lastActiveDot.nextSibling !== null) {
+
+ this.lastActiveDot.removeAttribute('data-is-active')
+ this.lastActiveDot.nextSibling.setAttribute('data-is-active', true)
+ this.lastActiveDot = this.lastActiveDot.nextSibling
}
},
diff --git a/resources/sass/app.scss b/resources/sass/app.scss
index a9424a2c..e37b6403 100644
--- a/resources/sass/app.scss
+++ b/resources/sass/app.scss
@@ -310,23 +310,17 @@ figure.no-icon {
background: hsl(0, 0%, 7%); /* grey */
}
-.dots li:nth-child(-n+27) {
+.dots li:nth-child(-n+9) {
background: hsl(48, 100%, 67%); /* yellow */
}
-.dots li:nth-child(-n+18) {
+.dots li:nth-child(-n+6) {
background: hsl(141, 71%, 48%); /* green */
}
-.dots li:nth-child(3n+2), .dots li:nth-child(3n+3) {
- //background-color: black;
- display:none;
-}
-
-.dots li:last-child() {
- // background-color: black;
- //display:none;
-}
+// .dots li:nth-child(3n+2), .dots li:nth-child(3n+3) {
+// display:none;
+// }
.input, .select select, .textarea {
background-color: hsl(0, 0%, 21%);