Fix #11 : Set TOTP loop duration on remaining time instead of remaining dots

This commit is contained in:
Bubka 2020-11-16 09:38:28 +01:00
parent 02798a05f3
commit 8253d28102
3 changed files with 84 additions and 73 deletions

View File

@ -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
*

View File

@ -7,7 +7,7 @@
<p class="is-size-6 has-text-grey has-ellipsis">{{ internal_account }}</p>
<p class="is-size-1 has-text-white is-clickable" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => token.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedToken }}</p>
<ul class="dots" v-if="internal_otpType === 'totp'">
<li v-for="n in 30"></li>
<li v-for="n in 10"></li>
</ul>
<ul v-else-if="internal_otpType === 'hotp'">
<li>counter: {{ internal_hotpCounter }}</li>
@ -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
}
},

View File

@ -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%);