M numberstation/otpurl.py => numberstation/otpurl.py +8 -1
@@ 20,6 20,7 @@ class OTPUrl:
self.issuer = qs['issuer'][0] if 'issuer' in qs else None
self.period = int(qs['period'][0]) if 'period' in qs else 30
self.digits = int(qs['digits'][0]) if 'digits' in qs else 6
+ self.initial_count = int(qs['initial_count'][0]) if 'initial_count' in qs else 0
self.digest = hashlib.sha1
if 'digest' in qs:
@@ 39,14 40,20 @@ class OTPUrl:
if self.type == 'totp':
totp = pyotp.TOTP(self.secret, interval=self.period, digits=self.digits, digest=self.digest)
return totp.now(), totp.interval - datetime.now().timestamp() % totp.interval
+ elif self.type == 'hotp':
+ hotp = pyotp.HOTP(self.secret, digits=self.digits, digest=self.digest, initial_count=self.initial_count)
+ return hotp.at(0), None
def get_url(self):
properties = {
'secret': self.secret,
'issuer': self.issuer,
- 'period': self.period,
'digits': self.digits
}
+ if self.type == 'totp':
+ properties['period'] = self.period
+ elif self.type == 'hotp':
+ properties['initial_count'] = self.initial_count
qs = urlencode(properties, doseq=True)
url = urlunparse(['otpauth', self.type, '/' + self.label, '', qs, ''])
return url
M numberstation/ui/style.css => numberstation/ui/style.css +6 -1
@@ 2,11 2,16 @@ label.token-label {
}
-label.token-code {
+label.token-code,
+button.token-code {
font-size: 2em;
font-family: monospace;
}
+button.token-code {
+ margin-top: 7px;
+}
+
label.errorbar {
background: #c00;
color: white;
M numberstation/window.py => numberstation/window.py +32 -4
@@ 137,13 137,22 @@ class NumberstationWindow:
user_code = ' '.join([code[i:i + 3] for i in range(0, len(code), 3)])
elif len(code) % 2 == 0:
user_code = ' '.join([code[i:i + 2] for i in range(0, len(code), 2)])
- label.set_text(user_code)
+ if token.type == 'totp':
+ label.set_text(user_code)
+ else:
+ label.set_label(user_code)
label.paste_code = paste_code
- progressbar.validity = int(validity)
+ if validity:
+ progressbar.validity = int(validity)
+ else:
+ if progressbar:
+ progressbar.validity = None
def update_codes(self):
GLib.timeout_add(1000, self.update_codes)
for timer in self.timers:
+ if timer.validity is None:
+ continue
timer.validity -= 1
if timer.validity < 1:
timer.validity = int(timer.period)
@@ 219,9 228,15 @@ class NumberstationWindow:
label.get_style_context().add_class('token-label')
grid.attach(label, 0, 0, 1, 1)
- code = Gtk.Label("000000")
+ if token.type == 'totp':
+ code = Gtk.Label("000000")
+ elif token.type == 'hotp':
+ code = Gtk.Button("000000")
+ code.set_hexpand(True)
+ code.connect('clicked', self.on_hotp_click)
code.get_style_context().add_class('token-code')
+ code.token = token
grid.attach(code, 0, 1, 1, 1)
timer = Gtk.ProgressBar()
timer.set_hexpand(True)
@@ 231,7 246,13 @@ class NumberstationWindow:
timer.token = token
timer.display = code
- timer.set_fraction(1.0 / token.period * timer.validity)
+ if timer.validity is not None:
+ timer.show()
+ timer.set_no_show_all(False)
+ timer.set_fraction(1.0 / token.period * timer.validity)
+ else:
+ timer.hide()
+ timer.set_no_show_all(True)
self.timers.append(timer)
grid.attach(timer, 0, 2, 1, 1)
eb.add(grid)
@@ 357,3 378,10 @@ class NumberstationWindow:
with open(filename, 'w') as handle:
handle.writelines(raw)
dialog.destroy()
+
+ def on_hotp_click(self, widget, *args):
+ clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
+ clipboard.set_text(widget.get_label().replace(' ', ''), -1)
+ widget.token.initial_count += 1
+ self.save_keyring()
+ self.update_code_label(widget, None, widget.token)