mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 08:44:40 +01:00
few missing files
This commit is contained in:
parent
0037c6d227
commit
f99922248d
43 changed files with 6081 additions and 0 deletions
20
Authenticator/__init__.py
Normal file
20
Authenticator/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
216
Authenticator/application.py
Normal file
216
Authenticator/application.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
require_version("GnomeKeyring", "1.0")
|
||||
from gi.repository import Gtk, GLib, Gio, Gdk, GObject, GnomeKeyring as GK
|
||||
from Authenticator.widgets.window import Window
|
||||
from Authenticator.models.database import Database
|
||||
from Authenticator.widgets.settings import SettingsWindow
|
||||
from Authenticator.models.settings import SettingsReader
|
||||
from Authenticator.interfaces.application_observrable import ApplicaitonObservable
|
||||
from Authenticator.utils import *
|
||||
import logging
|
||||
import signal
|
||||
from gettext import gettext as _
|
||||
from os import environ as env
|
||||
|
||||
|
||||
class Application(Gtk.Application):
|
||||
win = None
|
||||
alive = True
|
||||
locked = False
|
||||
settings_action = None
|
||||
|
||||
def __init__(self):
|
||||
Gtk.Application.__init__(self,
|
||||
application_id="org.gnome.Authenticator",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE)
|
||||
GLib.set_application_name(_("Gnome Authenticator"))
|
||||
GLib.set_prgname("Gnome Authenticator")
|
||||
|
||||
self.observable = ApplicaitonObservable()
|
||||
|
||||
self.menu = Gio.Menu()
|
||||
self.db = Database()
|
||||
self.cfg = SettingsReader()
|
||||
self.locked = self.cfg.read("state", "login")
|
||||
|
||||
result = GK.unlock_sync("org.gnome.Authenticator", None)
|
||||
if result == GK.Result.CANCELLED:
|
||||
self.quit()
|
||||
|
||||
if Gtk.get_major_version() >= 3 and Gtk.get_minor_version() >= 20:
|
||||
cssFileName = "org.gnome.Authenticator-post3.20.css"
|
||||
else:
|
||||
cssFileName = "org.gnome.Authenticator-pre3.20.css"
|
||||
cssProviderFile = Gio.File.new_for_uri(
|
||||
'resource:///org/gnome/Authenticator/%s' % cssFileName)
|
||||
cssProvider = Gtk.CssProvider()
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
try:
|
||||
cssProvider.load_from_file(cssProviderFile)
|
||||
styleContext.add_provider_for_screen(screen, cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
logging.debug("Loading css file ")
|
||||
except Exception as e:
|
||||
logging.error("Error message %s" % str(e))
|
||||
|
||||
def do_startup(self):
|
||||
Gtk.Application.do_startup(self)
|
||||
self.generate_menu()
|
||||
|
||||
def generate_menu(self):
|
||||
# Settings section
|
||||
settings_content = Gio.Menu.new()
|
||||
settings_content.append_item(
|
||||
Gio.MenuItem.new(_("Settings"), "app.settings"))
|
||||
self.is_dark_mode_menu = Gio.MenuItem.new(_("Night mode"), "app.night_mode")
|
||||
settings_content.append_item(self.is_dark_mode_menu)
|
||||
settings_section = Gio.MenuItem.new_section(None, settings_content)
|
||||
self.menu.append_item(settings_section)
|
||||
|
||||
# Help section
|
||||
help_content = Gio.Menu.new()
|
||||
if Gtk.get_major_version() >= 3 and Gtk.get_minor_version() >= 20:
|
||||
help_content.append_item(Gio.MenuItem.new(
|
||||
_("Shortcuts"), "app.shortcuts"))
|
||||
|
||||
help_content.append_item(Gio.MenuItem.new(_("About"), "app.about"))
|
||||
help_content.append_item(Gio.MenuItem.new(_("Quit"), "app.quit"))
|
||||
help_section = Gio.MenuItem.new_section(None, help_content)
|
||||
self.menu.append_item(help_section)
|
||||
|
||||
self.dark_mode_action = Gio.SimpleAction.new("night_mode", None)
|
||||
self.dark_mode_action.connect("activate", self.enable_dark_mode)
|
||||
self.add_action(self.dark_mode_action)
|
||||
|
||||
self.settings_action = Gio.SimpleAction.new("settings", None)
|
||||
self.settings_action.connect("activate", self.on_settings)
|
||||
self.settings_action.set_enabled(not self.locked)
|
||||
self.add_action(self.settings_action)
|
||||
|
||||
if Gtk.get_major_version() >= 3 and Gtk.get_minor_version() >= 20:
|
||||
action = Gio.SimpleAction.new("shortcuts", None)
|
||||
action.connect("activate", self.on_shortcuts)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("about", None)
|
||||
action.connect("activate", self.on_about)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("quit", None)
|
||||
action.connect("activate", self.on_quit)
|
||||
self.add_action(action)
|
||||
self.refresh_menu_night_mode()
|
||||
if not show_app_menu():
|
||||
self.set_app_menu(self.menu)
|
||||
logging.debug("Adding gnome shell menu")
|
||||
|
||||
def enable_dark_mode(self, *args):
|
||||
is_dark_mode = self.cfg.read("night-mode", "preferences")
|
||||
self.cfg.update("night-mode", not is_dark_mode, "preferences")
|
||||
self.refresh_menu_night_mode()
|
||||
|
||||
def refresh_menu_night_mode(self):
|
||||
is_dark_mode = self.cfg.read("night-mode", "preferences")
|
||||
settings = Gtk.Settings.get_default()
|
||||
settings.set_property("gtk-application-prefer-dark-theme", is_dark_mode)
|
||||
if is_dark_mode:
|
||||
self.is_dark_mode_menu.set_icon(Gio.ThemedIcon.new("emblem-ok-symbolic"))
|
||||
else:
|
||||
self.is_dark_mode_menu.set_icon(Gio.ThemedIcon.new(""))
|
||||
#self.dark_mode_action.set_state(GLib.Variant.new_boolean(is_dark_mode))
|
||||
|
||||
def do_activate(self, *args):
|
||||
if not self.win:
|
||||
self.win = Window(self)
|
||||
self.win.show_all()
|
||||
self.add_window(self.win)
|
||||
else:
|
||||
self.win.present()
|
||||
|
||||
def refresh_menu(self):
|
||||
is_enabled = self.settings_action.get_enabled()
|
||||
self.settings_action.set_enabled(not self.locked)
|
||||
|
||||
def on_shortcuts(self, *args):
|
||||
"""
|
||||
Shows keyboard shortcuts
|
||||
"""
|
||||
shortcuts = Application.shortcuts_dialog()
|
||||
if shortcuts:
|
||||
shortcuts.set_transient_for(self.win)
|
||||
shortcuts.show()
|
||||
|
||||
@staticmethod
|
||||
def shortcuts_dialog():
|
||||
if Gtk.get_major_version() >= 3 and Gtk.get_minor_version() >= 20:
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_resource('/org/gnome/Authenticator/shortcuts.ui')
|
||||
shortcuts = builder.get_object("shortcuts")
|
||||
return shortcuts
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def about_dialog():
|
||||
"""
|
||||
Shows about dialog
|
||||
"""
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_resource('/org/gnome/Authenticator/about.ui')
|
||||
|
||||
dialog = builder.get_object("AboutDialog")
|
||||
return dialog
|
||||
|
||||
def on_about(self, *args):
|
||||
"""
|
||||
Shows about dialog
|
||||
"""
|
||||
dialog = Application.about_dialog()
|
||||
dialog.set_transient_for(self.win)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def on_settings(self, *args):
|
||||
"""
|
||||
Shows settings window
|
||||
"""
|
||||
settings_window = SettingsWindow(self.win)
|
||||
settings_window.show_window()
|
||||
settings_window.present()
|
||||
|
||||
def on_quit(self, *args):
|
||||
"""
|
||||
Close the application, stops all threads
|
||||
and clear clipboard for safety reasons
|
||||
"""
|
||||
try:
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.clear()
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
self.observable.update_observers(alive=False)
|
||||
if self.win:
|
||||
self.win.save_window_state()
|
||||
self.win.destroy()
|
||||
self.quit()
|
13
Authenticator/interfaces/account_observrable.py
Normal file
13
Authenticator/interfaces/account_observrable.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from TwoFactorAuth.interfaces.observable import Observable
|
||||
|
||||
class AccountObservable(Observable):
|
||||
|
||||
def update_observers(self, *args, **kwargs):
|
||||
for observer in self.observers:
|
||||
observer.update(*args, **kwargs)
|
||||
|
||||
class AccountRowObservable(Observable):
|
||||
|
||||
def update_observers(self, *args, **kwargs):
|
||||
for observer in self.observers:
|
||||
observer.update(*args, **kwargs)
|
7
Authenticator/interfaces/application_observrable.py
Normal file
7
Authenticator/interfaces/application_observrable.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from TwoFactorAuth.interfaces.observable import Observable
|
||||
|
||||
class ApplicaitonObservable(Observable):
|
||||
|
||||
def update_observers(self, *args, **kwargs):
|
||||
for observer in self.observers:
|
||||
observer.update(*args, **kwargs)
|
20
Authenticator/interfaces/observable.py
Normal file
20
Authenticator/interfaces/observable.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
class Observable(object):
|
||||
|
||||
def __init__(self):
|
||||
self.observers = []
|
||||
|
||||
def register(self, observer):
|
||||
if not observer in self.observers:
|
||||
self.observers.append(observer)
|
||||
|
||||
def unregister(self, observer):
|
||||
if observer in self.observers:
|
||||
self.observers.remove(observer)
|
||||
|
||||
def unregister_all(self):
|
||||
if self.observers:
|
||||
del self.observers[:]
|
||||
|
||||
def update_observers(self, *args, **kwargs):
|
||||
for observer in self.observers:
|
||||
observer.update(*args, **kwargs)
|
110
Authenticator/models/account.py
Normal file
110
Authenticator/models/account.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
from Authenticator.models.settings import SettingsReader
|
||||
from Authenticator.models.code import Code
|
||||
from Authenticator.models.database import Database
|
||||
from Authenticator.models.observer import Observer
|
||||
from Authenticator.interfaces.account_observrable import AccountObservable, AccountRowObservable
|
||||
from gi.repository import GObject
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
import logging
|
||||
|
||||
class Account(GObject.GObject, Thread, Observer):
|
||||
__gsignals__ = {
|
||||
'code_updated': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
|
||||
'name_updated': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
|
||||
'counter_updated': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
|
||||
'removed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
|
||||
}
|
||||
counter_max = 30
|
||||
counter = 30
|
||||
alive = True
|
||||
code_generated = True
|
||||
|
||||
|
||||
def __init__(self, app, db):
|
||||
Thread.__init__(self)
|
||||
GObject.GObject.__init__(self)
|
||||
self.db = db
|
||||
cfg = SettingsReader()
|
||||
self.counter_max = cfg.read("refresh-time", "preferences")
|
||||
self.counter = self.counter_max
|
||||
self.account_id = app[0]
|
||||
self.account_name = app[1]
|
||||
self.secret_code = Database.fetch_secret_code(app[2])
|
||||
if self.secret_code:
|
||||
self.code = Code(self.secret_code)
|
||||
else:
|
||||
self.code_generated = False
|
||||
logging.error("Could not read the secret code,"
|
||||
"the keyring keys were reset manually")
|
||||
self.logo = app[3]
|
||||
self.account_observerable = AccountObservable()
|
||||
self.row_observerable = AccountRowObservable()
|
||||
self.start()
|
||||
|
||||
def do_name_updated(self, *args):
|
||||
self.account_observerable.update_observers(name=self.get_name())
|
||||
|
||||
def do_counter_updated(self, *args):
|
||||
self.account_observerable.update_observers(counter=self.get_counter())
|
||||
|
||||
def do_code_updated(self, *args):
|
||||
self.account_observerable.update_observers(code=self.get_code())
|
||||
|
||||
def do_removed(self, *args):
|
||||
self.row_observerable.update_observers(removed=self.get_id())
|
||||
|
||||
def update(self, alive):
|
||||
if not alive:
|
||||
self.kill()
|
||||
|
||||
def run(self):
|
||||
while self.code_generated and self.alive:
|
||||
self.counter -= 1
|
||||
if self.counter == 0:
|
||||
self.counter = self.counter_max
|
||||
self.code.update()
|
||||
self.emit("code_updated", True)
|
||||
self.emit("counter_updated", True)
|
||||
sleep(1)
|
||||
|
||||
def get_id(self):
|
||||
"""
|
||||
Get the application id
|
||||
:return: (int): row id
|
||||
"""
|
||||
return self.account_id
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
Get the application name
|
||||
:return: (str): application name
|
||||
"""
|
||||
return self.account_name
|
||||
|
||||
def get_logo(self):
|
||||
return self.logo
|
||||
|
||||
def get_code(self):
|
||||
return self.code.get_secret_code()
|
||||
|
||||
def get_counter(self):
|
||||
return self.counter
|
||||
|
||||
def get_counter_max(self):
|
||||
return self.counter_max
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Kill the row thread once it's removed
|
||||
"""
|
||||
self.alive = False
|
||||
|
||||
def remove(self):
|
||||
self.db.remove_by_id(self.get_id())
|
||||
self.emit("removed", True)
|
||||
|
||||
def set_name(self, name):
|
||||
self.db.update_name_by_id(self.get_id(), name)
|
||||
self.account_name = name
|
||||
self.emit("name_updated", True)
|
71
Authenticator/models/code.py
Normal file
71
Authenticator/models/code.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import logging
|
||||
import binascii
|
||||
from base64 import b32decode
|
||||
try:
|
||||
from pyotp import TOTP
|
||||
except ImportError:
|
||||
logging.error("Impossible to import TOTP, please install PyOTP first")
|
||||
|
||||
|
||||
class Code:
|
||||
password = None
|
||||
|
||||
def __init__(self, secret_code):
|
||||
self.secret_code = secret_code
|
||||
self.create()
|
||||
|
||||
@staticmethod
|
||||
def is_valid(code):
|
||||
"""
|
||||
Check if the secret code is a valid one
|
||||
"""
|
||||
try:
|
||||
b32decode(code, casefold=True)
|
||||
return True
|
||||
except (binascii.Error, ValueError):
|
||||
return False
|
||||
|
||||
def create(self):
|
||||
"""
|
||||
Create a tfa code
|
||||
"""
|
||||
try:
|
||||
self.totp = TOTP(self.secret_code)
|
||||
self.password = self.totp.now()
|
||||
except Exception as e:
|
||||
logging.error("Couldn't generate two factor code : %s" % str(e))
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Update the code
|
||||
"""
|
||||
self.password = self.totp.now()
|
||||
|
||||
def get_secret_code(self):
|
||||
try:
|
||||
if self.password:
|
||||
return self.password
|
||||
else:
|
||||
raise AttributeError
|
||||
except AttributeError as e:
|
||||
logging.error("Couldn't generate the code : %s " % str(e))
|
||||
return None
|
195
Authenticator/models/database.py
Normal file
195
Authenticator/models/database.py
Normal file
|
@ -0,0 +1,195 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import sqlite3
|
||||
import logging
|
||||
from gi.repository import GnomeKeyring as GK, GLib
|
||||
from hashlib import sha256
|
||||
from Authenticator.utils import create_file
|
||||
|
||||
|
||||
class Database:
|
||||
|
||||
def __init__(self):
|
||||
database_file = GLib.get_home_dir() + '/.config/Authenticator/database.db'
|
||||
if create_file(database_file):
|
||||
logging.debug("Creating database file %s " % database_file)
|
||||
self.conn = sqlite3.connect(database_file)
|
||||
if not self.is_table_exists():
|
||||
logging.debug(
|
||||
"SQL: Table 'accounts' does not exists, creating it now...")
|
||||
self.create_table()
|
||||
logging.debug("SQL: Table 'accounts' created successfully")
|
||||
|
||||
@staticmethod
|
||||
def fetch_secret_code(secret_code):
|
||||
attr = GK.Attribute.list_new()
|
||||
GK.Attribute.list_append_string(attr, 'id', secret_code)
|
||||
result, value = GK.find_items_sync(GK.ItemType.GENERIC_SECRET, attr)
|
||||
if result == GK.Result.OK:
|
||||
return value[0].secret
|
||||
else:
|
||||
return None
|
||||
|
||||
def add_account(self, name, secret_code, image):
|
||||
"""
|
||||
Add an account to accounts table
|
||||
:param name: (str) account name
|
||||
:param secret_code: (str) ASCII Secret code
|
||||
:param image: image path or icon name
|
||||
:return:
|
||||
"""
|
||||
encrypted_secret = sha256(secret_code.encode('utf-8')).hexdigest()
|
||||
t = (name, encrypted_secret, image,)
|
||||
query = "INSERT INTO accounts (name, secret_code, image) VALUES (?, ?, ?)"
|
||||
try:
|
||||
GK.create_sync("org.gnome.Authenticator", None)
|
||||
attr = GK.Attribute.list_new()
|
||||
GK.Attribute.list_append_string(attr, 'id', encrypted_secret)
|
||||
GK.Attribute.list_append_string(attr, 'secret_code', secret_code)
|
||||
GK.item_create_sync("org.gnome.Authenticator", GK.ItemType.GENERIC_SECRET, repr(encrypted_secret), attr,
|
||||
secret_code, False)
|
||||
self.conn.execute(query, t)
|
||||
self.conn.commit()
|
||||
uid = self.get_latest_id()
|
||||
return [uid, name, encrypted_secret, image]
|
||||
except Exception as e:
|
||||
logging.error("SQL: Couldn't add a new account : %s ", str(e))
|
||||
|
||||
def get_secret_code(self, uid):
|
||||
"""
|
||||
Count number of accounts
|
||||
:return: (int) count
|
||||
"""
|
||||
c = self.conn.cursor()
|
||||
query = "SELECT secret_code FROM accounts WHERE uid=?"
|
||||
try:
|
||||
data = c.execute(query, (uid,))
|
||||
return data.fetchone()[0]
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
"SQL: Couldn't get account secret code : %s " % str(e))
|
||||
return None
|
||||
|
||||
def remove_by_id(self, uid):
|
||||
"""
|
||||
Remove an account by uid
|
||||
:param uid: (int) account uid
|
||||
:return:
|
||||
"""
|
||||
secret_code = self.get_secret_code(uid)
|
||||
if secret_code:
|
||||
found = False
|
||||
(result, ids) = GK.list_item_ids_sync("org.gnome.Authenticator")
|
||||
for gid in ids:
|
||||
(result, item) = GK.item_get_info_sync("org.gnome.Authenticator", gid)
|
||||
if result == GK.Result.OK:
|
||||
if item.get_display_name().strip("'") == secret_code:
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
GK.item_delete_sync("org.gnome.Authenticator", gid)
|
||||
query = "DELETE FROM accounts WHERE uid=?"
|
||||
try:
|
||||
self.conn.execute(query, (uid,))
|
||||
self.conn.commit()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
"SQL: Couldn't remove account by uid : %s with error : %s" % (uid, str(e)))
|
||||
|
||||
def update_name_by_id(self, id, new_name):
|
||||
query = "UPDATE accounts SET name=? WHERE uid=?"
|
||||
try:
|
||||
self.conn.execute(query, (new_name, id, ))
|
||||
self.conn.commit()
|
||||
except Exception as e:
|
||||
logging.error("SQL: couldn't update account name by id : %s with error : %s" %(id, str(e)))
|
||||
|
||||
def count(self):
|
||||
"""
|
||||
Count number of accounts
|
||||
:return: (int) count
|
||||
"""
|
||||
c = self.conn.cursor()
|
||||
query = "SELECT COUNT(uid) AS count FROM accounts"
|
||||
try:
|
||||
data = c.execute(query)
|
||||
return data.fetchone()[0]
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
"SQL: Couldn't count accounts list : %s " % str(e))
|
||||
return None
|
||||
|
||||
def fetch_apps(self):
|
||||
"""
|
||||
Fetch list of accounts
|
||||
:return: (tuple) list of accounts
|
||||
"""
|
||||
c = self.conn.cursor()
|
||||
query = "SELECT * FROM accounts"
|
||||
try:
|
||||
data = c.execute(query)
|
||||
return data.fetchall()
|
||||
except Exception as e:
|
||||
logging.error("SQL: Couldn't fetch accounts list %s" % str(e))
|
||||
return None
|
||||
|
||||
def get_latest_id(self):
|
||||
"""
|
||||
Get the latest uid on accounts table
|
||||
:return: (int) latest uid
|
||||
"""
|
||||
c = self.conn.cursor()
|
||||
query = "SELECT uid FROM accounts ORDER BY uid DESC LIMIT 1;"
|
||||
try:
|
||||
data = c.execute(query)
|
||||
return data.fetchone()[0]
|
||||
except Exception as e:
|
||||
logging.error("SQL: Couldn't fetch the latest uid %s" % str(e))
|
||||
return None
|
||||
|
||||
def create_table(self):
|
||||
"""
|
||||
Create accounts table
|
||||
"""
|
||||
query = '''CREATE TABLE "accounts" (
|
||||
"uid" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE ,
|
||||
"name" VARCHAR NOT NULL ,
|
||||
"secret_code" VARCHAR NOT NULL ,
|
||||
"image" TEXT NOT NULL
|
||||
)'''
|
||||
try:
|
||||
self.conn.execute(query)
|
||||
self.conn.commit()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
"SQL: impossible to create table 'accounts' %s " % str(e))
|
||||
|
||||
def is_table_exists(self):
|
||||
"""
|
||||
Check if accounts table exists
|
||||
:return: (bool)
|
||||
"""
|
||||
query = "SELECT uid from accounts LIMIT 1"
|
||||
c = self.conn.cursor()
|
||||
try:
|
||||
data = c.execute(query)
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
8
Authenticator/models/observer.py
Normal file
8
Authenticator/models/observer.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
class Observer(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def update(self, *args, **kwargs):
|
||||
pass
|
65
Authenticator/models/qr_reader.py
Normal file
65
Authenticator/models/qr_reader.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from PIL import Image
|
||||
from zbarlight import scan_codes
|
||||
from urllib.parse import urlparse, parse_qsl
|
||||
import logging
|
||||
from os import remove, path
|
||||
from Authenticator.models.code import Code
|
||||
|
||||
|
||||
class QRReader:
|
||||
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
|
||||
def read(self):
|
||||
with open(self.filename, 'rb') as image_file:
|
||||
image = Image.open(image_file)
|
||||
image.load()
|
||||
self.codes = scan_codes('qrcode', image)
|
||||
self.remove()
|
||||
if self.codes:
|
||||
otpauth_url = self.codes[0].decode()
|
||||
self.codes = dict(parse_qsl(urlparse(otpauth_url)[4]))
|
||||
return self.codes
|
||||
else:
|
||||
logging.error("Invalid QR image")
|
||||
return None
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
remove image file for security reasons
|
||||
"""
|
||||
if path.isfile(self.filename):
|
||||
remove(self.filename)
|
||||
logging.debug("QR code image was removed for security reasons")
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
Validate if the QR code is a valid tfa
|
||||
"""
|
||||
if isinstance(self.codes, dict):
|
||||
if set(["issuer", "secret"]).issubset(self.codes.keys()):
|
||||
return Code.is_valid(self.codes["secret"])
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
68
Authenticator/models/settings.py
Normal file
68
Authenticator/models/settings.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi.repository import Gtk, Gio
|
||||
import logging
|
||||
|
||||
|
||||
class SettingsReader:
|
||||
path = "org.gnome.Authenticator"
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
# Check if the gsettings path exists
|
||||
self.source = Gio.SettingsSchemaSource.get_default()
|
||||
self.source.lookup(self.path, True)
|
||||
except Exception as e:
|
||||
logging.critical("Couldn't load gsettings source %s " % str(e))
|
||||
|
||||
def read(self, key, path):
|
||||
"""
|
||||
Read a 'key' from org.gnome.Authenticator.'path'
|
||||
:param key: (str) key to read
|
||||
:param path: login/user
|
||||
:return: value
|
||||
"""
|
||||
gsettings = Gio.Settings.new(self.path + "." + path)
|
||||
value = gsettings.get_value(key)
|
||||
value_type = value.get_type_string()
|
||||
value = str(value).strip("'")
|
||||
if value_type == "i":
|
||||
return int(value)
|
||||
elif value_type == "b":
|
||||
return value == "true"
|
||||
else:
|
||||
return value
|
||||
|
||||
def update(self, key, value, path):
|
||||
"""
|
||||
Update 'key' value to 'value' from org.gnome.Authenticator.'path'
|
||||
:param key: (str) key to read
|
||||
:param value: updated value
|
||||
:param path: login/user
|
||||
:return: value
|
||||
"""
|
||||
gsettings = Gio.Settings.new(self.path + "." + path)
|
||||
if type(value) is int:
|
||||
gsettings.set_int(key, value)
|
||||
elif type(value) is bool:
|
||||
gsettings.set_boolean(key, value)
|
||||
else:
|
||||
gsettings.set_string(key, value)
|
||||
gsettings.apply()
|
112
Authenticator/utils.py
Normal file
112
Authenticator/utils.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from os import path, mknod, makedirs, environ as env
|
||||
from gi.repository import GdkPixbuf, Gtk, Gio, GLib
|
||||
import logging
|
||||
from subprocess import PIPE, Popen, call
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
|
||||
def is_gnome():
|
||||
"""
|
||||
Check if the current distro is gnome
|
||||
"""
|
||||
return env.get("XDG_CURRENT_DESKTOP").lower() == "gnome"
|
||||
|
||||
|
||||
def show_app_menu():
|
||||
try:
|
||||
source = Gio.SettingsSchemaSource.get_default()
|
||||
path = "org.gnome.desktop.wm.preferences"
|
||||
key = "button-layout"
|
||||
source.lookup(path, True)
|
||||
gsettings = Gio.Settings.new(path)
|
||||
value = gsettings.get_value(key)
|
||||
value = str(value).strip("'")
|
||||
# "appmenu" in value or
|
||||
return not is_gnome()
|
||||
except Exception as e:
|
||||
logging.critical("Couldn't load gsettings source %s " % str(e))
|
||||
|
||||
|
||||
def get_icon(image, size):
|
||||
"""
|
||||
Generate a GdkPixbuf image
|
||||
:param image: icon name or image path
|
||||
:return: GdkPixbux Image
|
||||
"""
|
||||
directory = path.join(env.get("DATA_DIR"), "applications", "images") + "/"
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
if theme.has_icon(path.splitext(image)[0]):
|
||||
icon = theme.load_icon(path.splitext(image)[0], size, 0)
|
||||
elif path.exists(directory + image):
|
||||
icon = GdkPixbuf.Pixbuf.new_from_file(directory + image)
|
||||
elif path.exists(image):
|
||||
icon = GdkPixbuf.Pixbuf.new_from_file(image)
|
||||
else:
|
||||
icon = theme.load_icon("image-missing", size, 0)
|
||||
if icon.get_width() != size or icon.get_height() != size:
|
||||
icon = icon.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR)
|
||||
return icon
|
||||
|
||||
|
||||
def create_file(file_path):
|
||||
"""
|
||||
Create a file and create parent folder if missing
|
||||
"""
|
||||
if not (path.isfile(file_path) and path.exists(file_path)):
|
||||
dirs = file_path.split("/")
|
||||
i = 0
|
||||
while i < len(dirs) - 1:
|
||||
directory = "/".join(dirs[0:i + 1]).strip()
|
||||
if not path.exists(directory) and len(directory) != 0:
|
||||
makedirs(directory)
|
||||
logging.debug("Creating directory %s " % directory)
|
||||
i += 1
|
||||
mknod(file_path)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def screenshot_area():
|
||||
"""
|
||||
Screenshot an area of the screen using gnome-screenshot
|
||||
used to QR scan
|
||||
"""
|
||||
ink_flag = call(['which', 'gnome-screenshot'], stdout=PIPE, stderr=PIPE)
|
||||
if ink_flag == 0:
|
||||
file_name = path.join(GLib.get_tmp_dir(), NamedTemporaryFile().name)
|
||||
p = Popen(["gnome-screenshot", "-a", "-f", file_name],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
output, error = p.communicate()
|
||||
if error:
|
||||
error = error.decode("utf-8").split("\n")
|
||||
logging.error("\n".join([e for e in error]))
|
||||
if not path.isfile(file_name):
|
||||
logging.debug("The screenshot was not token")
|
||||
return False
|
||||
return file_name
|
||||
else:
|
||||
logging.error(
|
||||
"Couldn't find gnome-screenshot, please install it first")
|
||||
return False
|
||||
|
442
Authenticator/widgets/account_row.py
Normal file
442
Authenticator/widgets/account_row.py
Normal file
|
@ -0,0 +1,442 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, Pango
|
||||
from Authenticator.widgets.confirmation import ConfirmationMessage
|
||||
from Authenticator.utils import get_icon
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
import logging
|
||||
from gettext import gettext as _
|
||||
from Authenticator.models.observer import Observer
|
||||
|
||||
|
||||
class RowEntryName(Gtk.Entry):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.generate()
|
||||
|
||||
def generate(self):
|
||||
Gtk.Entry.__init__(self, xalign=0)
|
||||
self.set_text(self.name)
|
||||
self.set_width_chars(25)
|
||||
self.set_max_width_chars(25)
|
||||
self.hide()
|
||||
|
||||
def toggle(self, visible):
|
||||
self.set_visible(visible)
|
||||
self.set_no_show_all(not visible)
|
||||
|
||||
def is_visible(self):
|
||||
return self.get_visible()
|
||||
|
||||
def hide(self):
|
||||
self.toggle(False)
|
||||
|
||||
def show(self):
|
||||
self.toggle(True)
|
||||
|
||||
def focus(self):
|
||||
self.grab_focus_without_selecting()
|
||||
|
||||
|
||||
class AccountRow(Observer):
|
||||
notification = None
|
||||
timer = 0
|
||||
remove_timer = 0
|
||||
|
||||
def __init__(self, parent, window, account):
|
||||
# Read default values
|
||||
self.window = window
|
||||
self.parent = parent
|
||||
self.account = account
|
||||
self.account.account_observerable.register(self)
|
||||
# Create needed widgets
|
||||
self.code_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.revealer = Gtk.Revealer()
|
||||
self.checkbox = Gtk.CheckButton()
|
||||
self.application_name = Gtk.Label(xalign=0)
|
||||
self.code_label = Gtk.Label(xalign=0)
|
||||
self.timer_label = Gtk.Label(xalign=0)
|
||||
self.is_grid_view = isinstance(self, AccountRowGrid)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
self.set_account_code(kwargs.get("code", None))
|
||||
self.set_account_name(kwargs.get("name", None))
|
||||
self.set_account_counter(kwargs.get("counter", None))
|
||||
|
||||
def set_account_name(self, name):
|
||||
if name:
|
||||
self.application_name.props.tooltip_text = name
|
||||
self.application_name .set_text(name)
|
||||
|
||||
def set_account_code(self, code):
|
||||
if code:
|
||||
self.code_label.set_text(str(code))
|
||||
|
||||
def set_account_counter(self, counter):
|
||||
if counter:
|
||||
if self.is_grid_view:
|
||||
label = str(counter)
|
||||
else:
|
||||
label = _("Expires in %s seconds" % str(counter))
|
||||
self.timer_label.set_label(label)
|
||||
|
||||
def get_code_label(self):
|
||||
return self.code_label
|
||||
|
||||
def get_name(self):
|
||||
return self.account.get_name()
|
||||
|
||||
def get_checkbox(self):
|
||||
"""
|
||||
Get ListBowRow's checkbox
|
||||
:return: (Gtk.Checkbox)
|
||||
"""
|
||||
return self.checkbox
|
||||
|
||||
def get_code_box(self):
|
||||
"""
|
||||
Get code's box
|
||||
:return: (Gtk.Box)
|
||||
"""
|
||||
return self.code_box
|
||||
|
||||
def toggle_code_box(self):
|
||||
"""
|
||||
Toggle code box
|
||||
"""
|
||||
self.revealer.set_reveal_child(not self.revealer.get_reveal_child())
|
||||
|
||||
def copy_code(self, *args):
|
||||
"""
|
||||
Copy code shows the code box for a while (10s by default)
|
||||
"""
|
||||
self.timer = 0
|
||||
code = self.account.get_code()
|
||||
try:
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
self.window.notification.update(
|
||||
_('Code "{0}" copied to clipboard'.format(str(code))))
|
||||
self.window.notification.show()
|
||||
clipboard.clear()
|
||||
clipboard.set_text(code, len(code))
|
||||
logging.debug("Secret code copied to clipboard")
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
self.revealer.set_reveal_child(True)
|
||||
GLib.timeout_add_seconds(1, self.update_timer)
|
||||
|
||||
def update_remove_countdown(self, *args):
|
||||
if self.remove_timer > 0:
|
||||
self.remove_timer -= 1
|
||||
if self.remove_timer == 0:
|
||||
self.account.remove()
|
||||
return self.timer <= self.window.notification.timeout
|
||||
|
||||
def update_timer(self, *args):
|
||||
"""
|
||||
Update timer
|
||||
"""
|
||||
self.timer += 1
|
||||
if self.timer > 10:
|
||||
self.revealer.set_reveal_child(False)
|
||||
return self.timer <= 10
|
||||
|
||||
def toggle_code(self, *args):
|
||||
self.toggle_code_box()
|
||||
|
||||
def toggle_action_box(self, visible):
|
||||
self.actions_box.set_visible(visible)
|
||||
self.actions_box.set_no_show_all(not visible)
|
||||
|
||||
def remove(self, *args):
|
||||
"""
|
||||
Remove an account
|
||||
"""
|
||||
message = _('Do you really want to remove "%s"?' % self.account.get_name())
|
||||
confirmation = ConfirmationMessage(self.window, message)
|
||||
confirmation.show()
|
||||
if confirmation.get_confirmation():
|
||||
self.window.notification.update(_('"%s" was removed' % self.account.get_name()), self.undo_remove)
|
||||
self.window.notification.show()
|
||||
self.remove_timer = self.window.notification.timeout
|
||||
GLib.timeout_add_seconds(1, self.update_remove_countdown)
|
||||
confirmation.destroy()
|
||||
|
||||
|
||||
def undo_remove(self):
|
||||
self.remove_timer = 0
|
||||
self.window.notification.hide()
|
||||
|
||||
class AccountRowList(AccountRow, Gtk.ListBoxRow):
|
||||
|
||||
def __init__(self, parent, window, account):
|
||||
AccountRow.__init__(self, parent, window, account)
|
||||
Gtk.ListBoxRow.__init__(self)
|
||||
self.create_row()
|
||||
self.window.connect("key-press-event", self.__on_key_press)
|
||||
|
||||
def create_row(self):
|
||||
"""
|
||||
Create ListBoxRow
|
||||
"""
|
||||
self.get_style_context().add_class("application-list-row")
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
h_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
box.pack_start(h_box, True, True, 0)
|
||||
box.pack_start(self.revealer, True, True, 0)
|
||||
|
||||
# Checkbox
|
||||
self.checkbox.set_visible(False)
|
||||
self.checkbox.set_no_show_all(True)
|
||||
self.checkbox.connect("toggled", self.parent.select_account)
|
||||
h_box.pack_start(self.checkbox, False, True, 6)
|
||||
|
||||
# account logo
|
||||
auth_icon = get_icon(self.account.get_logo(), 48)
|
||||
auth_logo = Gtk.Image(xalign=0)
|
||||
auth_logo.set_from_pixbuf(auth_icon)
|
||||
h_box.pack_start(auth_logo, False, True, 6)
|
||||
|
||||
# Account name entry
|
||||
name_entry_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.name_entry = RowEntryName(self.account.get_name())
|
||||
name_entry_box.pack_start(self.name_entry, False, False, 6)
|
||||
h_box.pack_start(name_entry_box, False, False, 0)
|
||||
name_entry_box.set_visible(False)
|
||||
|
||||
# accout name
|
||||
name_event = Gtk.EventBox()
|
||||
self.application_name .get_style_context().add_class("application-name")
|
||||
self.application_name.set_ellipsize(Pango.EllipsizeMode.END)
|
||||
self.set_account_name(self.account.get_name())
|
||||
name_event.connect("button-press-event", self.toggle_code)
|
||||
name_event.add(self.application_name)
|
||||
h_box.pack_start(name_event, False, True, 6)
|
||||
|
||||
# Remove button
|
||||
self.actions_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
remove_event = Gtk.EventBox()
|
||||
remove_button = Gtk.Image(xalign=0)
|
||||
remove_button.set_from_icon_name("user-trash-symbolic",
|
||||
Gtk.IconSize.SMALL_TOOLBAR)
|
||||
remove_button.set_tooltip_text(_("Remove the account"))
|
||||
remove_event.add(remove_button)
|
||||
remove_event.connect("button-press-event", self.remove)
|
||||
self.actions_box.pack_end(remove_event, False, False, 6)
|
||||
|
||||
# Copy button
|
||||
copy_event = Gtk.EventBox()
|
||||
copy_button = Gtk.Image(xalign=0)
|
||||
copy_button.set_from_icon_name(
|
||||
"edit-copy-symbolic", Gtk.IconSize.SMALL_TOOLBAR)
|
||||
copy_button.set_tooltip_text(_("Copy the generated code"))
|
||||
copy_event.connect("button-press-event", self.copy_code)
|
||||
copy_event.add(copy_button)
|
||||
self.actions_box.pack_end(copy_event, False, False, 6)
|
||||
|
||||
# Edit button
|
||||
edit_event = Gtk.EventBox()
|
||||
self.edit_button = Gtk.Image(xalign=0)
|
||||
self.edit_button.set_from_icon_name("document-edit-symbolic",
|
||||
Gtk.IconSize.SMALL_TOOLBAR)
|
||||
self.edit_button.set_tooltip_text(_("Edit the account"))
|
||||
edit_event.add(self.edit_button)
|
||||
edit_event.connect("button-press-event", self.edit)
|
||||
self.actions_box.pack_end(edit_event, False, False, 6)
|
||||
|
||||
# Apply button
|
||||
apply_event = Gtk.EventBox()
|
||||
self.apply_button = Gtk.Image(xalign=0)
|
||||
self.apply_button.set_from_icon_name("emblem-ok-symbolic",
|
||||
Gtk.IconSize.SMALL_TOOLBAR)
|
||||
self.apply_button.set_tooltip_text(_("Save the new account name"))
|
||||
apply_event.add(self.apply_button)
|
||||
apply_event.connect("button-press-event", self.apply_edit_name)
|
||||
self.actions_box.pack_end(apply_event, False, False, 6)
|
||||
h_box.pack_end(self.actions_box, False, False, 0)
|
||||
self.toggle_action_box(True)
|
||||
self.toggle_edit_mode(False)
|
||||
|
||||
self.set_account_counter(self.account.get_counter())
|
||||
self.timer_label.get_style_context().add_class("account-timer")
|
||||
self.code_label.get_style_context().add_class("account-secret-code")
|
||||
if self.account.code_generated:
|
||||
self.set_account_code(self.account.get_code())
|
||||
else:
|
||||
self.set_account_code(_("Error during the generation of code"))
|
||||
|
||||
self.code_box.pack_end(self.timer_label, False, True, 6)
|
||||
self.code_box.pack_start(self.code_label, False, True, 6)
|
||||
self.revealer.add(self.code_box)
|
||||
self.revealer.set_reveal_child(False)
|
||||
self.add(box)
|
||||
|
||||
def toggle_edit_mode(self, visible):
|
||||
if visible:
|
||||
self.name_entry.show()
|
||||
self.name_entry.set_text(self.account.get_name())
|
||||
self.name_entry.focus()
|
||||
else:
|
||||
self.name_entry.hide()
|
||||
self.application_name.set_visible(not visible)
|
||||
self.application_name.set_no_show_all(visible)
|
||||
self.apply_button.set_visible(visible)
|
||||
self.apply_button.set_no_show_all(not visible)
|
||||
self.edit_button.get_parent().set_visible(not visible)
|
||||
self.edit_button.get_parent().set_no_show_all(visible)
|
||||
|
||||
def __on_key_press(self, widget, event):
|
||||
keyname = Gdk.keyval_name(event.keyval).lower()
|
||||
if not self.window.is_locked():
|
||||
if self.parent.get_selected_row_id() == self.account.get_id():
|
||||
is_search_bar = self.window.search_bar.is_visible()
|
||||
is_editing_name = self.name_entry.is_visible()
|
||||
if keyname == "delete" and not is_search_bar and not is_editing_name:
|
||||
self.remove()
|
||||
return True
|
||||
|
||||
if keyname == "escape":
|
||||
if is_editing_name:
|
||||
self.toggle_edit_mode(False)
|
||||
return True
|
||||
|
||||
if keyname == "return":
|
||||
if is_editing_name:
|
||||
self.apply_edit_name()
|
||||
else:
|
||||
self.toggle_code_box()
|
||||
return True
|
||||
|
||||
if event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
if keyname == 'c':
|
||||
self.copy_code()
|
||||
return True
|
||||
return False
|
||||
|
||||
def edit(self, *args):
|
||||
is_visible = self.name_entry.is_visible()
|
||||
self.toggle_edit_mode(not is_visible)
|
||||
self.parent.select_row(self)
|
||||
|
||||
def apply_edit_name(self, *args):
|
||||
new_name = self.name_entry.get_text()
|
||||
self.application_name.set_text(new_name)
|
||||
self.account.set_name(new_name)
|
||||
self.toggle_edit_mode(False)
|
||||
|
||||
|
||||
class AccountRowGrid(AccountRow, Gtk.Box):
|
||||
|
||||
def __init__(self, parent, window, account):
|
||||
AccountRow.__init__(self, parent, window, account)
|
||||
Gtk.Box.__init__(self)
|
||||
self.create_row()
|
||||
self.window.connect("key-press-event", self.__on_key_press)
|
||||
|
||||
def create_row(self):
|
||||
"""
|
||||
Create ListBoxRow
|
||||
"""
|
||||
self.actions_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
v_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Checkbox
|
||||
overlay_box = Gtk.Overlay()
|
||||
checkbox_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.checkbox.set_visible(False)
|
||||
self.checkbox.get_style_context().add_class("checkbutton-grid")
|
||||
self.checkbox.set_no_show_all(True)
|
||||
self.checkbox.connect("toggled", self.parent.select_account)
|
||||
checkbox_box.pack_end(self.checkbox, False, False, 0)
|
||||
overlay_box.add_overlay(checkbox_box)
|
||||
|
||||
# account logo
|
||||
logo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
auth_icon = get_icon(self.account.get_logo(), 96)
|
||||
auth_logo = Gtk.Image(xalign=0)
|
||||
auth_logo.set_from_pixbuf(auth_icon)
|
||||
logo_box.add(auth_logo)
|
||||
overlay_box.add(logo_box)
|
||||
v_box.pack_start(overlay_box, False, False, 6)
|
||||
|
||||
# accout name
|
||||
name_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
name_event = Gtk.EventBox()
|
||||
self.application_name .get_style_context().add_class("application-name-grid")
|
||||
self.application_name.set_ellipsize(Pango.EllipsizeMode.END)
|
||||
self.set_account_name(self.account.get_name())
|
||||
name_event.connect("button-press-event", self.toggle_code)
|
||||
name_event.add(self.application_name)
|
||||
name_box.pack_start(name_event, True, False, 6)
|
||||
v_box.pack_start(name_box, False, False, 0)
|
||||
|
||||
# Copy button
|
||||
copy_event = Gtk.EventBox()
|
||||
copy_button = Gtk.Image(xalign=0)
|
||||
copy_button.set_from_icon_name(
|
||||
"edit-copy-symbolic", Gtk.IconSize.SMALL_TOOLBAR)
|
||||
copy_button.set_tooltip_text(_("Copy the generated code"))
|
||||
copy_event.connect("button-press-event", self.copy_code)
|
||||
copy_event.add(copy_button)
|
||||
self.actions_box.pack_end(copy_event, False, False, 6)
|
||||
|
||||
|
||||
self.set_account_counter(self.account.get_counter())
|
||||
self.timer_label.get_style_context().add_class("account-timer-grid")
|
||||
|
||||
self.code_label.get_style_context().add_class("account-secret-code-grid")
|
||||
if self.account.code_generated:
|
||||
self.set_account_code(self.account.get_code())
|
||||
else:
|
||||
self.set_account_code(_("Error during the generation of code"))
|
||||
|
||||
self.code_box.pack_end(self.timer_label, False, True, 6)
|
||||
self.code_box.pack_start(self.code_label, True, False, 6)
|
||||
self.revealer.add(self.code_box)
|
||||
self.revealer.set_reveal_child(False)
|
||||
v_box.pack_start(self.revealer, False, False, 0)
|
||||
|
||||
# self.pack_start(checkbox_box, False, False, 0)
|
||||
self.pack_start(v_box, True, False, 0)
|
||||
|
||||
|
||||
def __on_key_press(self, widget, event):
|
||||
keyname = Gdk.keyval_name(event.keyval).lower()
|
||||
if not self.window.is_locked():
|
||||
if self.parent.get_selected_row_id() == self.account.get_id():
|
||||
is_search_bar = self.window.search_bar.is_visible()
|
||||
if keyname == "delete" and not is_search_bar:
|
||||
self.remove()
|
||||
return True
|
||||
|
||||
if keyname == "return":
|
||||
self.toggle_code_box()
|
||||
return True
|
||||
|
||||
if event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
if keyname == 'c':
|
||||
self.copy_code()
|
||||
return True
|
||||
return False
|
168
Authenticator/widgets/accounts_grid.py
Normal file
168
Authenticator/widgets/accounts_grid.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
|
||||
from Authenticator.widgets.confirmation import ConfirmationMessage
|
||||
from Authenticator.widgets.account_row import AccountRowGrid
|
||||
from gettext import gettext as _
|
||||
from hashlib import sha256
|
||||
import logging
|
||||
|
||||
|
||||
class AccountsGrid(Gtk.FlowBox):
|
||||
scrolled_win = None
|
||||
|
||||
def __init__(self, window, accounts):
|
||||
self.accounts = accounts
|
||||
self.window = window
|
||||
self.generate()
|
||||
self.connect("child-activated", self.activate_child)
|
||||
self.connect("selected-children-changed", self.selected_child)
|
||||
GLib.timeout_add_seconds(1, self.refresh)
|
||||
|
||||
def generate(self):
|
||||
Gtk.FlowBox.__init__(self,orientation=Gtk.Orientation.HORIZONTAL)
|
||||
# Create a ScrolledWindow for accounts
|
||||
self.set_min_children_per_line(2)
|
||||
self.set_max_children_per_line(8)
|
||||
self.set_valign(Gtk.Align.START)
|
||||
self.set_column_spacing(0)
|
||||
self.set_row_spacing(0)
|
||||
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
|
||||
self.scrolled_win = Gtk.ScrolledWindow()
|
||||
self.scrolled_win.add_with_viewport(self)
|
||||
self.set_homogeneous(True)
|
||||
count = len(self.accounts)
|
||||
|
||||
for account in self.accounts:
|
||||
self.add(AccountRowGrid(self, self.window, account))
|
||||
|
||||
if count != 0:
|
||||
self.select_child(self.get_child_at_index(0))
|
||||
|
||||
self.show_all()
|
||||
|
||||
def selected_child(self, account_list):
|
||||
for box in self.get_children():
|
||||
row = box.get_children()[0]
|
||||
checkbutton = row.get_checkbox()
|
||||
if not checkbutton.get_active() and self.window.is_select_mode:
|
||||
self.unselect_child(box)
|
||||
|
||||
def activate_child(self, account_list, selected_child):
|
||||
if self.window.is_select_mode and selected_child:
|
||||
selected_box = selected_child.get_children()[0]
|
||||
self.select_account(selected_box.get_checkbox())
|
||||
|
||||
|
||||
def toggle_select_mode(self):
|
||||
is_select_mode = self.window.is_select_mode
|
||||
if is_select_mode:
|
||||
self.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
else:
|
||||
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
if len(self.get_children()) != 0:
|
||||
self.select_child(self.get_child_at_index(0))
|
||||
|
||||
for box in self.get_children():
|
||||
row = box.get_children()[0]
|
||||
checkbox = row.get_checkbox()
|
||||
code_label = row.get_code_label()
|
||||
visible = checkbox.get_visible()
|
||||
style_context = code_label.get_style_context()
|
||||
if is_select_mode:
|
||||
self.select_account(checkbox)
|
||||
row.toggle_action_box(visible)
|
||||
checkbox.set_visible(not visible)
|
||||
checkbox.set_no_show_all(visible)
|
||||
|
||||
def remove_selected(self, *args):
|
||||
"""
|
||||
Remove selected accounts
|
||||
"""
|
||||
for row in self.get_selected_children():
|
||||
checkbox = row.get_checkbox()
|
||||
if checkbox.get_active():
|
||||
row.remove()
|
||||
self.unselect_all()
|
||||
self.window.toggle_select()
|
||||
self.window.refresh_window()
|
||||
|
||||
def select_account(self, checkbutton):
|
||||
"""
|
||||
Select an account
|
||||
:param checkbutton:
|
||||
"""
|
||||
is_active = checkbutton.get_active()
|
||||
is_visible = checkbutton.get_visible()
|
||||
flowbox_child = checkbutton.get_parent().get_parent().get_parent().get_parent().get_parent()
|
||||
if is_active:
|
||||
self.select_child(flowbox_child)
|
||||
else:
|
||||
self.unselect_child(flowbox_child)
|
||||
selected_count = len(self.get_selected_children())
|
||||
self.window.hb.remove_button.set_sensitive(selected_count > 0)
|
||||
|
||||
def get_selected_child_id(self):
|
||||
selected_box = self.get_selected_children()
|
||||
if selected_box:
|
||||
return selected_box[0].get_children()[0].account.get_id()
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_selected_row_id(self):
|
||||
return self.get_selected_child_id()
|
||||
|
||||
def toggle(self, visible):
|
||||
self.set_visible(visible)
|
||||
self.set_no_show_all(not visible)
|
||||
self.get_scrolled_win().set_visible(visible)
|
||||
self.get_scrolled_win().set_no_show_all(not visible)
|
||||
|
||||
def is_visible(self):
|
||||
return self.get_visible()
|
||||
|
||||
def hide(self):
|
||||
self.toggle(False)
|
||||
|
||||
def show(self):
|
||||
self.toggle(True)
|
||||
|
||||
def get_scrolled_win(self):
|
||||
return self.scrolled_win
|
||||
|
||||
def refresh(self):
|
||||
self.scrolled_win.hide()
|
||||
self.scrolled_win.show_all()
|
||||
|
||||
def append(self, account):
|
||||
self.add(AccountRowGrid(self, self.window, account))
|
||||
self.refresh()
|
||||
|
||||
def remove_by_id(self, _id):
|
||||
for row in self.get_children():
|
||||
row = row.get_children()[0]
|
||||
if row.account.get_id() == _id:
|
||||
self.remove(row)
|
||||
break
|
||||
self.refresh()
|
182
Authenticator/widgets/accounts_list.py
Normal file
182
Authenticator/widgets/accounts_list.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
|
||||
from Authenticator.widgets.confirmation import ConfirmationMessage
|
||||
from Authenticator.widgets.account_row import AccountRowList
|
||||
from gettext import gettext as _
|
||||
from hashlib import sha256
|
||||
import logging
|
||||
|
||||
|
||||
class AccountsList(Gtk.ListBox):
|
||||
scrolled_win = None
|
||||
|
||||
def __init__(self, window, accounts):
|
||||
self.accounts = accounts
|
||||
self.window = window
|
||||
self.generate()
|
||||
self.window.connect("key-press-event", self.on_key_press)
|
||||
self.connect("row-activated", self.activate_row)
|
||||
self.connect("row-selected", self.selected_row)
|
||||
|
||||
GLib.timeout_add_seconds(1, self.refresh)
|
||||
|
||||
def generate(self):
|
||||
Gtk.ListBox.__init__(self)
|
||||
# Create a ScrolledWindow for accounts
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
|
||||
self.get_style_context().add_class("applications-list")
|
||||
self.set_adjustment()
|
||||
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
box.pack_start(self, True, True, 0)
|
||||
|
||||
self.scrolled_win = Gtk.ScrolledWindow()
|
||||
self.scrolled_win.add_with_viewport(box)
|
||||
|
||||
count = len(self.accounts)
|
||||
|
||||
for account in self.accounts:
|
||||
self.add(AccountRowList(self, self.window, account))
|
||||
|
||||
if count != 0:
|
||||
self.select_row(self.get_row_at_index(0))
|
||||
self.show_all()
|
||||
|
||||
def selected_row(self, account_list, selected_row):
|
||||
for row in self.get_children():
|
||||
if row != selected_row:
|
||||
row.toggle_edit_mode(False)
|
||||
checkbutton = row.get_checkbox()
|
||||
if not checkbutton.get_active() and self.window.is_select_mode:
|
||||
self.unselect_row(row)
|
||||
|
||||
def activate_row(self, account_list, selected_row):
|
||||
if self.window.is_select_mode and selected_row:
|
||||
self.select_account(selected_row.get_checkbox())
|
||||
|
||||
def on_key_press(self, app, key_event):
|
||||
"""
|
||||
Keyboard Listener handling
|
||||
"""
|
||||
keyname = Gdk.keyval_name(key_event.keyval).lower()
|
||||
if not self.window.is_locked():
|
||||
if not self.window.no_account_box.is_visible():
|
||||
if keyname == "up" or keyname == "down":
|
||||
count = len(self.get_children())
|
||||
dx = -1 if keyname == "up" else 1
|
||||
selected_row = self.get_selected_row()
|
||||
if selected_row is not None:
|
||||
index = selected_row.get_index()
|
||||
new_index = (index + dx) % count
|
||||
self.select_row(self.get_row_at_index(new_index))
|
||||
return True
|
||||
return False
|
||||
|
||||
def toggle_select_mode(self):
|
||||
is_select_mode = self.window.is_select_mode
|
||||
if is_select_mode:
|
||||
self.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
else:
|
||||
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
if len(self.get_children()) != 0:
|
||||
self.select_row(self.get_row_at_index(0))
|
||||
|
||||
for row in self.get_children():
|
||||
checkbox = row.get_checkbox()
|
||||
code_label = row.get_code_label()
|
||||
visible = checkbox.get_visible()
|
||||
style_context = code_label.get_style_context()
|
||||
if is_select_mode:
|
||||
self.select_account(checkbox)
|
||||
style_context.add_class("application-secret-code-select-mode")
|
||||
else:
|
||||
style_context.remove_class("application-secret-code-select-mode")
|
||||
row.toggle_action_box(visible)
|
||||
checkbox.set_visible(not visible)
|
||||
checkbox.set_no_show_all(visible)
|
||||
|
||||
def remove_selected(self, *args):
|
||||
"""
|
||||
Remove selected accounts
|
||||
"""
|
||||
for row in self.get_selected_rows():
|
||||
checkbox = row.get_checkbox()
|
||||
if checkbox.get_active():
|
||||
row.remove()
|
||||
self.unselect_all()
|
||||
self.window.toggle_select()
|
||||
|
||||
|
||||
def select_account(self, checkbutton):
|
||||
"""
|
||||
Select an account
|
||||
:param checkbutton:
|
||||
"""
|
||||
is_active = checkbutton.get_active()
|
||||
is_visible = checkbutton.get_visible()
|
||||
listbox_row = checkbutton.get_parent().get_parent().get_parent()
|
||||
if is_active:
|
||||
self.select_row(listbox_row)
|
||||
else:
|
||||
self.unselect_row(listbox_row)
|
||||
selected_count = len(self.get_selected_rows())
|
||||
self.window.hb.remove_button.set_sensitive(selected_count > 0)
|
||||
|
||||
def get_selected_row_id(self):
|
||||
selected_row = self.get_selected_row()
|
||||
if selected_row:
|
||||
return selected_row.account.get_id()
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_scrolled_win(self):
|
||||
return self.scrolled_win
|
||||
|
||||
def toggle(self, visible):
|
||||
self.get_scrolled_win().set_visible(visible)
|
||||
self.get_scrolled_win().set_no_show_all(not visible)
|
||||
self.set_visible(visible)
|
||||
self.set_no_show_all(not visible)
|
||||
|
||||
def is_visible(self):
|
||||
return self.get_visible()
|
||||
|
||||
def hide(self):
|
||||
self.toggle(False)
|
||||
|
||||
def show(self):
|
||||
self.toggle(True)
|
||||
|
||||
def refresh(self):
|
||||
self.scrolled_win.hide()
|
||||
self.scrolled_win.show_all()
|
||||
|
||||
def append(self, account):
|
||||
self.add(AccountRowList(self, self.window, account))
|
||||
|
||||
def remove_by_id(self, _id):
|
||||
for row in self.get_children():
|
||||
if row.account.get_id() == _id:
|
||||
self.remove(row)
|
||||
break
|
||||
self.refresh()
|
147
Authenticator/widgets/accounts_window.py
Normal file
147
Authenticator/widgets/accounts_window.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
|
||||
from Authenticator.widgets.accounts_list import AccountsList
|
||||
from Authenticator.widgets.accounts_grid import AccountsGrid
|
||||
from Authenticator.widgets.account_row import AccountRowGrid, AccountRowList
|
||||
from Authenticator.widgets.search_bar import SearchBar
|
||||
from Authenticator.models.account import Account
|
||||
from gettext import gettext as _
|
||||
from hashlib import sha256
|
||||
import logging
|
||||
from Authenticator.models.observer import Observer
|
||||
|
||||
class AccountsWindow(Gtk.Box, Observer):
|
||||
|
||||
def __init__(self, application, window):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
|
||||
self.app = application
|
||||
self.window = window
|
||||
self.scrolled_win = None
|
||||
self.generate()
|
||||
|
||||
def generate(self):
|
||||
self.generate_accounts_list()
|
||||
self.generate_search_bar()
|
||||
self.pack_start(self.search_bar, False, True, 0)
|
||||
self.reorder_child(self.search_bar, 0)
|
||||
|
||||
def generate_accounts_list(self):
|
||||
"""
|
||||
Generate an account ListBox inside of a ScrolledWindow
|
||||
"""
|
||||
apps = self.app.db.fetch_apps()
|
||||
count = len(apps)
|
||||
self.accounts = []
|
||||
for app in apps:
|
||||
account = Account(app, self.app.db)
|
||||
account.row_observerable.register(self)
|
||||
self.accounts.append(account)
|
||||
self.app.observable.register(account)
|
||||
|
||||
self.accounts_list = AccountsList(self.window, self.accounts)
|
||||
self.accounts_grid = AccountsGrid(self.window, self.accounts)
|
||||
|
||||
self.pack_start(self.accounts_list.get_scrolled_win(), True, True, 0)
|
||||
self.pack_start(self.accounts_grid.get_scrolled_win(), True, True, 0)
|
||||
is_grid = self.app.cfg.read(
|
||||
"view-mode", "preferences").lower() == "grid"
|
||||
self.set_mode_view(is_grid)
|
||||
|
||||
def generate_search_bar(self):
|
||||
"""
|
||||
Generate search bar box and entry
|
||||
"""
|
||||
self.search_bar = SearchBar(self.window, self.window.hb.search_button,
|
||||
[self.accounts_list, self.accounts_grid])
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
removed_id = kwargs.get("removed", None)
|
||||
unlocked = kwargs.pop("unlocked", None)
|
||||
locked = kwargs.pop("locked", None)
|
||||
counter = kwargs.pop("counter", None)
|
||||
view_mode = kwargs.pop("view_mode", None)
|
||||
if counter == 0 or locked:
|
||||
self.hide()
|
||||
elif unlocked or counter != 0:
|
||||
self.show()
|
||||
if removed_id:
|
||||
self.accounts_list.remove_by_id(removed_id)
|
||||
self.accounts_grid.remove_by_id(removed_id)
|
||||
self.window.emit("changed", True)
|
||||
if view_mode:
|
||||
self.set_mode_view(view_mode == "grid")
|
||||
|
||||
|
||||
def get_accounts_list(self):
|
||||
return self.accounts_list
|
||||
|
||||
def get_accounts_grid(self):
|
||||
return self.accounts_grid
|
||||
|
||||
def set_mode_view(self, is_grid):
|
||||
if is_grid:
|
||||
self.scrolled_win = self.accounts_grid.get_scrolled_win()
|
||||
self.accounts_list.hide()
|
||||
self.accounts_grid.show()
|
||||
self.accounts_grid.refresh()
|
||||
else:
|
||||
self.scrolled_win = self.accounts_list.get_scrolled_win()
|
||||
self.accounts_grid.hide()
|
||||
self.accounts_list.show()
|
||||
self.accounts_list.refresh()
|
||||
self.scrolled_win.set_no_show_all(False)
|
||||
self.scrolled_win.set_visible(True)
|
||||
|
||||
def get_search_bar(self):
|
||||
return self.search_bar
|
||||
|
||||
def toggle(self, visible):
|
||||
self.set_visible(visible)
|
||||
self.set_no_show_all(not visible)
|
||||
|
||||
def is_visible(self):
|
||||
return self.get_visible()
|
||||
|
||||
def hide(self):
|
||||
self.toggle(False)
|
||||
|
||||
def show(self):
|
||||
self.toggle(True)
|
||||
|
||||
def refresh(self, *args):
|
||||
self.accounts_list.refresh()
|
||||
self.accounts_grid.refresh()
|
||||
|
||||
def append(self, app):
|
||||
"""
|
||||
Add an element to the ListBox
|
||||
"""
|
||||
if app:
|
||||
account = Account(app, self.app.db)
|
||||
account.row_observerable.register(self)
|
||||
self.accounts.append(account)
|
||||
self.app.observable.register(account)
|
||||
self.accounts_list.append(account)
|
||||
self.accounts_grid.append(account)
|
||||
self.window.emit("changed", True)
|
225
Authenticator/widgets/add_account.py
Normal file
225
Authenticator/widgets/add_account.py
Normal file
|
@ -0,0 +1,225 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from Authenticator.utils import screenshot_area
|
||||
from Authenticator.widgets.applications_list import ApplicationChooserWindow
|
||||
from Authenticator.widgets.inapp_notification import InAppNotification
|
||||
from Authenticator.models.code import Code
|
||||
from Authenticator.models.qr_reader import QRReader
|
||||
from Authenticator.utils import get_icon
|
||||
from gettext import gettext as _
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, Gio
|
||||
|
||||
|
||||
class AddAccount(Gtk.Window):
|
||||
|
||||
def __init__(self, window):
|
||||
self.parent = window
|
||||
|
||||
self.selected_logo = None
|
||||
self.step = 1
|
||||
self.account_image = Gtk.Image(xalign=0)
|
||||
self.secret_code = Gtk.Entry()
|
||||
self.name_entry = Gtk.Entry()
|
||||
self.hb = Gtk.HeaderBar()
|
||||
|
||||
self.generate_window()
|
||||
self.generate_components()
|
||||
self.generate_header_bar()
|
||||
|
||||
def generate_window(self):
|
||||
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL,
|
||||
title=_("Add a new account"),
|
||||
modal=True, destroy_with_parent=True)
|
||||
self.connect("delete-event", self.close_window)
|
||||
self.resize(500, 400)
|
||||
self.set_border_width(18)
|
||||
self.set_size_request(500, 300)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||
self.set_resizable(False)
|
||||
self.set_transient_for(self.parent)
|
||||
self.notification = InAppNotification()
|
||||
|
||||
self.connect("key_press_event", self.on_key_press)
|
||||
|
||||
def generate_header_bar(self):
|
||||
"""
|
||||
Generate the header bar box
|
||||
"""
|
||||
self.hb.props.title = _("Add a new account")
|
||||
left_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
right_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
cancel_button = Gtk.Button.new_with_label(_("Cancel"))
|
||||
cancel_button.connect("clicked", self.close_window)
|
||||
cancel_button.get_style_context().add_class("destructive-action")
|
||||
left_box.add(cancel_button)
|
||||
|
||||
self.apply_button = Gtk.Button.new_with_label(_("Add"))
|
||||
self.apply_button.get_style_context().add_class("suggested-action")
|
||||
self.apply_button.connect("clicked", self.add_account)
|
||||
self.apply_button.set_sensitive(False)
|
||||
|
||||
qr_button = Gtk.Button()
|
||||
qr_icon = Gio.ThemedIcon(name="camera-photo-symbolic")
|
||||
qr_image = Gtk.Image.new_from_gicon(qr_icon, Gtk.IconSize.BUTTON)
|
||||
qr_button.set_tooltip_text(_("Scan a QR code"))
|
||||
qr_button.set_image(qr_image)
|
||||
qr_button.connect("clicked", self.on_qr_scan)
|
||||
|
||||
right_box.add(qr_button)
|
||||
right_box.add(self.apply_button)
|
||||
|
||||
self.hb.pack_start(left_box)
|
||||
self.hb.pack_end(right_box)
|
||||
self.set_titlebar(self.hb)
|
||||
|
||||
def generate_components(self):
|
||||
"""
|
||||
Generate window components
|
||||
"""
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
labels_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
logo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
hbox_name = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=18)
|
||||
account_name = Gtk.Label()
|
||||
account_name.set_text(_("Account Name"))
|
||||
|
||||
hbox_name.pack_end(self.name_entry, False, True, 0)
|
||||
hbox_name.pack_end(account_name, False, True, 0)
|
||||
|
||||
hbox_secret_code = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=18)
|
||||
secret_code_label = Gtk.Label()
|
||||
secret_code_label.set_text(_("Secret Code"))
|
||||
self.secret_code.connect("changed", self.validate_ascii_code)
|
||||
|
||||
hbox_secret_code.pack_end(self.secret_code, False, True, 0)
|
||||
hbox_secret_code.pack_end(secret_code_label, False, True, 0)
|
||||
|
||||
account_logo = get_icon("image-missing", 48)
|
||||
self.account_image.set_from_pixbuf(account_logo)
|
||||
self.account_image.get_style_context().add_class("application-logo-add")
|
||||
logo_box.pack_start(self.account_image, True, False, 6)
|
||||
logo_box.set_property("margin-bottom", 20)
|
||||
|
||||
vbox.add(hbox_name)
|
||||
vbox.add(hbox_secret_code)
|
||||
labels_box.pack_start(vbox, True, False, 6)
|
||||
main_box.pack_start(self.notification, False, False, 0)
|
||||
main_box.pack_start(logo_box, False, True, 6)
|
||||
main_box.pack_start(labels_box, False, True, 6)
|
||||
self.add(main_box)
|
||||
|
||||
def on_qr_scan(self, *args):
|
||||
filename = screenshot_area()
|
||||
if filename:
|
||||
qr = QRReader(filename)
|
||||
data = qr.read()
|
||||
if qr.is_valid():
|
||||
self.name_entry.set_text(data["issuer"])
|
||||
self.secret_code.set_text(data["secret"])
|
||||
self.apply_button.set_sensitive(True)
|
||||
else:
|
||||
self.notification.update(
|
||||
_("Selected area is not a valid QR code"))
|
||||
self.notification.set_message_type(Gtk.MessageType.ERROR)
|
||||
self.notification.show()
|
||||
|
||||
def on_key_press(self, key, key_event):
|
||||
"""
|
||||
Keyboard Listener handler
|
||||
"""
|
||||
key_name = Gdk.keyval_name(key_event.keyval).lower()
|
||||
if key_name == "escape":
|
||||
self.close_window()
|
||||
return True
|
||||
|
||||
if key_name == "return":
|
||||
if self.apply_button.get_sensitive():
|
||||
self.add_account()
|
||||
return True
|
||||
|
||||
if key_event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
if key_name == 's':
|
||||
self.on_qr_scan()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_logo(self, image):
|
||||
"""
|
||||
Update image logo
|
||||
"""
|
||||
self.selected_logo = image
|
||||
account_icon = get_icon(image, 48)
|
||||
self.account_image.clear()
|
||||
self.account_image.set_from_pixbuf(account_icon)
|
||||
|
||||
def validate_ascii_code(self, entry):
|
||||
"""
|
||||
Validate if the typed secret code is a valid ascii one
|
||||
"""
|
||||
ascii_code = entry.get_text().strip()
|
||||
is_valid = Code.is_valid(ascii_code)
|
||||
self.apply_button.set_sensitive(is_valid)
|
||||
if not is_valid and len(ascii_code) != 0:
|
||||
entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, "dialog-error-symbolic")
|
||||
else:
|
||||
entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, None)
|
||||
|
||||
def add_account(self, *args):
|
||||
"""
|
||||
Add a new application to the database
|
||||
"""
|
||||
name = self.name_entry.get_text()
|
||||
secret_code = self.secret_code.get_text()
|
||||
logo = self.selected_logo if self.selected_logo else "image-missing"
|
||||
try:
|
||||
new_account = self.parent.app.db.add_account(name, secret_code, logo)
|
||||
self.parent.accounts_box.append(new_account)
|
||||
self.close_window()
|
||||
except Exception as e:
|
||||
logging.error("Error in addin.accounts_boxg a new account")
|
||||
logging.error(str(e))
|
||||
|
||||
def show_window(self):
|
||||
if self.step == 1:
|
||||
applications_choose_window = ApplicationChooserWindow(self)
|
||||
applications_choose_window.show_window()
|
||||
applications_choose_window.present()
|
||||
self.step = 2
|
||||
else:
|
||||
self.secret_code.grab_focus_without_selecting()
|
||||
self.show_all()
|
||||
|
||||
def close_window(self, *args):
|
||||
"""
|
||||
Close the window
|
||||
"""
|
||||
self.destroy()
|
68
Authenticator/widgets/application_row.py
Normal file
68
Authenticator/widgets/application_row.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
from Authenticator.utils import get_icon
|
||||
from gettext import gettext as _
|
||||
import logging
|
||||
|
||||
|
||||
class ApplicationRow(Gtk.ListBoxRow):
|
||||
|
||||
def __init__(self, name, image):
|
||||
Gtk.ListBoxRow.__init__(self)
|
||||
self.name = name
|
||||
self.image = image
|
||||
# Create the list row
|
||||
self.create_row()
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
Get the application label
|
||||
:return: (str): application label
|
||||
"""
|
||||
return self.name
|
||||
|
||||
def get_icon_name(self):
|
||||
return self.image
|
||||
|
||||
def create_row(self):
|
||||
"""
|
||||
Create ListBoxRow
|
||||
"""
|
||||
self.get_style_context().add_class("application-list-row")
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
# Application logo
|
||||
application_logo = get_icon(self.image, 48)
|
||||
application_image = Gtk.Image(xalign=0)
|
||||
application_image.set_from_pixbuf(application_logo)
|
||||
hbox.pack_start(application_image, False, True, 6)
|
||||
|
||||
# Application name
|
||||
application_name = Gtk.Label(xalign=0)
|
||||
application_name.get_style_context().add_class("application-name")
|
||||
application_name.set_text(self.name)
|
||||
hbox.pack_start(application_name, True, True, 6)
|
||||
|
||||
vbox.pack_start(hbox, True, True, 6)
|
||||
self.add(vbox)
|
245
Authenticator/widgets/applications_list.py
Normal file
245
Authenticator/widgets/applications_list.py
Normal file
|
@ -0,0 +1,245 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GObject, Gio, Gdk, GLib
|
||||
from Authenticator.widgets.search_bar import SearchBar
|
||||
from Authenticator.widgets.application_row import ApplicationRow
|
||||
from os import path, environ as env
|
||||
from gettext import gettext as _
|
||||
import yaml
|
||||
from glob import glob
|
||||
from threading import Thread
|
||||
import logging
|
||||
|
||||
|
||||
class ApplicationChooserWindow(Gtk.Window, Thread, GObject.GObject):
|
||||
__gsignals__ = {
|
||||
'db_updated': (GObject.SignalFlags.RUN_LAST, None, (bool,))
|
||||
}
|
||||
|
||||
def __init__(self, window):
|
||||
Thread.__init__(self)
|
||||
GObject.GObject.__init__(self)
|
||||
self.nom = "applications-db-reader"
|
||||
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL, modal=True,
|
||||
destroy_with_parent=True)
|
||||
self.parent = window
|
||||
self.db = []
|
||||
self.spinner = Gtk.Spinner()
|
||||
self.search_button = Gtk.ToggleButton()
|
||||
self.listbox = Gtk.ListBox()
|
||||
self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.generate_window()
|
||||
self.generate_search_bar()
|
||||
self.generate_components()
|
||||
self.generate_header_bar()
|
||||
self.start()
|
||||
|
||||
def emit(self, *args):
|
||||
GLib.idle_add(GObject.GObject.emit, self, *args)
|
||||
|
||||
def run(self):
|
||||
# Load applications list using a Thread
|
||||
self.read_database()
|
||||
self.add_apps()
|
||||
self.emit("db_updated", True)
|
||||
|
||||
def generate_window(self):
|
||||
"""
|
||||
Generate the main window
|
||||
"""
|
||||
self.connect("destroy", self.close_window)
|
||||
self.resize(500, 650)
|
||||
self.set_size_request(500, 650)
|
||||
x, y = self.parent.parent.get_position()
|
||||
if x and y:
|
||||
self.move(x, y)
|
||||
self.set_resizable(False)
|
||||
self.set_transient_for(self.parent.parent)
|
||||
self.connect("key_press_event", self.on_key_press)
|
||||
self.add(self.main_box)
|
||||
|
||||
def generate_components(self):
|
||||
"""
|
||||
Generate window compenents
|
||||
"""
|
||||
box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
# Create a ScrolledWindow
|
||||
self.scrolled_win = Gtk.ScrolledWindow()
|
||||
self.scrolled_win.add_with_viewport(box_outer)
|
||||
self.scrolled_win.hide()
|
||||
self.main_box.pack_start(self.scrolled_win, True, True, 0)
|
||||
|
||||
self.listbox.get_style_context().add_class("applications-list")
|
||||
self.listbox.set_adjustment()
|
||||
self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
box_outer.pack_start(self.listbox, True, True, 0)
|
||||
|
||||
self.spinner_box_outer = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL)
|
||||
spinner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.spinner.start()
|
||||
self.spinner.show()
|
||||
spinner_box.pack_start(self.spinner, False, False, 6)
|
||||
self.spinner_box_outer.pack_start(spinner_box, True, True, 6)
|
||||
self.main_box.pack_start(self.spinner_box_outer, True, True, 0)
|
||||
|
||||
def generate_header_bar(self):
|
||||
"""
|
||||
Generate header bar box
|
||||
"""
|
||||
self.hb = Gtk.HeaderBar()
|
||||
self.hb.props.title = _("Select an application")
|
||||
|
||||
left_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
right_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
cancel_button = Gtk.Button.new_with_label(_("Cancel"))
|
||||
cancel_button.connect("clicked", self.close_window)
|
||||
left_box.add(cancel_button)
|
||||
|
||||
search_icon = Gio.ThemedIcon(name="system-search-symbolic")
|
||||
search_image = Gtk.Image.new_from_gicon(
|
||||
search_icon, Gtk.IconSize.BUTTON)
|
||||
self.search_button.set_tooltip_text(_("Search"))
|
||||
self.search_button.set_image(search_image)
|
||||
|
||||
next_button = Gtk.Button.new_with_label(_("Next"))
|
||||
next_button.get_style_context().add_class("suggested-action")
|
||||
next_button.connect("clicked", self.select_application)
|
||||
|
||||
right_box.pack_start(self.search_button, False, False, 6)
|
||||
right_box.pack_start(next_button, False, False, 6)
|
||||
|
||||
self.hb.pack_end(right_box)
|
||||
self.hb.pack_start(left_box)
|
||||
self.set_titlebar(self.hb)
|
||||
|
||||
def generate_search_bar(self):
|
||||
"""
|
||||
Generate the search bar
|
||||
"""
|
||||
self.search_bar = SearchBar(self, self.search_button, [self.listbox])
|
||||
self.main_box.pack_start(self.search_bar, False, True, 0)
|
||||
|
||||
def is_valid_app(self, app):
|
||||
"""
|
||||
Check if the application supports tfa
|
||||
"""
|
||||
if set(["tfa", "software"]).issubset(app.keys()):
|
||||
return app["tfa"] and app["software"]
|
||||
else:
|
||||
return False
|
||||
|
||||
def on_key_press(self, label, key_event):
|
||||
"""
|
||||
Keyboard listener handling
|
||||
"""
|
||||
keyname = Gdk.keyval_name(key_event.keyval).lower()
|
||||
|
||||
if keyname == "escape":
|
||||
if not self.search_bar.is_visible():
|
||||
self.close_window()
|
||||
return True
|
||||
|
||||
if keyname == "up" or keyname == "down":
|
||||
dx = -1 if keyname == "up" else 1
|
||||
index = self.listbox.get_selected_row().get_index()
|
||||
index = (index + dx) % len(self.db)
|
||||
selected_row = self.listbox.get_row_at_index(index)
|
||||
self.listbox.select_row(selected_row)
|
||||
return True
|
||||
|
||||
if keyname == "return":
|
||||
self.select_application()
|
||||
return True
|
||||
return False
|
||||
|
||||
def do_db_updated(self, *args):
|
||||
"""
|
||||
Hide and stop the spinner and show the scrolled window
|
||||
"""
|
||||
self.spinner.stop()
|
||||
self.spinner_box_outer.hide()
|
||||
self.scrolled_win.show()
|
||||
self.listbox.hide()
|
||||
if len(self.listbox.get_children()) != 0:
|
||||
self.listbox.show_all()
|
||||
logging.debug("UI updated")
|
||||
|
||||
def read_database(self):
|
||||
"""
|
||||
Read .yml database files provided by 2factorauth guys!
|
||||
"""
|
||||
db_dir = path.join(env.get("DATA_DIR"), "applications") + "/data/*.yml"
|
||||
logging.debug("Database folder is {0}".format(db_dir))
|
||||
db_files = glob(db_dir)
|
||||
logging.debug("Reading database files started")
|
||||
for db_file in db_files:
|
||||
with open(db_file, 'r') as data:
|
||||
try:
|
||||
websites = yaml.load(data)["websites"]
|
||||
for app in websites:
|
||||
if self.is_valid_app(app):
|
||||
self.db.append(app)
|
||||
except yaml.YAMLError as error:
|
||||
logging.error("Error loading yml file : %s " % str(error))
|
||||
logging.debug("Reading database files finished")
|
||||
|
||||
def add_apps(self):
|
||||
"""
|
||||
Add database applications to the Gtk.ListBox
|
||||
"""
|
||||
self.db = sorted(self.db, key=lambda k: k['name'].lower())
|
||||
logging.debug("Application list was ordered alphabetically")
|
||||
|
||||
for app in self.db:
|
||||
img_path = app["img"]
|
||||
app_name = app["name"]
|
||||
self.listbox.add(ApplicationRow(app_name, img_path))
|
||||
|
||||
if len(self.db) != 0:
|
||||
self.listbox.select_row(self.listbox.get_row_at_index(0))
|
||||
|
||||
def select_application(self, *args):
|
||||
"""
|
||||
Select a logo and return its path to the add application window
|
||||
"""
|
||||
selected_row = self.listbox.get_selected_row()
|
||||
if selected_row:
|
||||
img_path = selected_row.get_icon_name()
|
||||
app_name = selected_row.get_name()
|
||||
logging.debug("%s was selected" % app_name)
|
||||
self.parent.update_logo(img_path)
|
||||
self.parent.name_entry.set_text(app_name)
|
||||
self.parent.show_window()
|
||||
self.parent.present()
|
||||
self.close_window()
|
||||
|
||||
def show_window(self):
|
||||
self.show_all()
|
||||
|
||||
def close_window(self, *args):
|
||||
"""
|
||||
Close the window
|
||||
"""
|
||||
logging.debug("Closing ApplicationChooserWindow")
|
||||
self.destroy()
|
181
Authenticator/widgets/change_password.py
Normal file
181
Authenticator/widgets/change_password.py
Normal file
|
@ -0,0 +1,181 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
from Authenticator.models.settings import SettingsReader
|
||||
from gettext import gettext as _
|
||||
from hashlib import sha256
|
||||
import logging
|
||||
|
||||
|
||||
class PasswordWindow(Gtk.Window):
|
||||
|
||||
def __init__(self, window):
|
||||
self.parent = window
|
||||
self.cfg = SettingsReader()
|
||||
|
||||
self.hb = Gtk.HeaderBar()
|
||||
self.apply_button = Gtk.Button.new_with_label(_("Apply"))
|
||||
self.new_entry = Gtk.Entry()
|
||||
self.new2_entry = Gtk.Entry()
|
||||
self.old_entry = Gtk.Entry()
|
||||
|
||||
self.generate_window()
|
||||
self.generate_components()
|
||||
self.generate_header_bar()
|
||||
|
||||
def generate_window(self):
|
||||
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL, title=_("Change password"),
|
||||
modal=True, destroy_with_parent=True)
|
||||
self.connect("delete-event", self.close_window)
|
||||
self.resize(300, 100)
|
||||
self.set_border_width(18)
|
||||
self.set_size_request(300, 100)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||
self.set_resizable(False)
|
||||
self.set_transient_for(self.parent)
|
||||
self.connect("key_press_event", self.on_key_press)
|
||||
|
||||
def show_window(self):
|
||||
self.show_all()
|
||||
|
||||
def on_key_press(self, key, key_event):
|
||||
"""
|
||||
Keyboard listener handler
|
||||
"""
|
||||
if Gdk.keyval_name(key_event.keyval) == "Escape":
|
||||
self.close_window()
|
||||
|
||||
def generate_components(self):
|
||||
"""
|
||||
Generate window components
|
||||
"""
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
|
||||
if len(self.cfg.read("password", "login")) != 0:
|
||||
box_old = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=18)
|
||||
old_label = Gtk.Label()
|
||||
old_label.set_text(_("Old password"))
|
||||
self.old_entry.connect("changed", self.on_type_password)
|
||||
self.old_entry.set_visibility(False)
|
||||
box_old.pack_end(self.old_entry, False, True, 0)
|
||||
box_old.pack_end(old_label, False, True, 0)
|
||||
box.add(box_old)
|
||||
|
||||
box_new = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=18)
|
||||
new_label = Gtk.Label()
|
||||
new_label.set_text(_("New password"))
|
||||
|
||||
self.new_entry.connect("changed", self.on_type_password)
|
||||
self.new_entry.set_visibility(False)
|
||||
box_new.pack_end(self.new_entry, False, True, 0)
|
||||
box_new.pack_end(new_label, False, True, 0)
|
||||
|
||||
box_new2 = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, spacing=18)
|
||||
new2_label = Gtk.Label()
|
||||
new2_label.set_text(_("Repeat new password"))
|
||||
self.new2_entry.connect("changed", self.on_type_password)
|
||||
self.new2_entry.set_visibility(False)
|
||||
box_new2.pack_end(self.new2_entry, False, True, 0)
|
||||
box_new2.pack_end(new2_label, False, True, 0)
|
||||
|
||||
box.add(box_new)
|
||||
box.add(box_new2)
|
||||
|
||||
main_box.pack_start(box, False, True, 6)
|
||||
self.add(main_box)
|
||||
|
||||
def update_password(self, *args):
|
||||
"""
|
||||
Update user password
|
||||
"""
|
||||
password = sha256(
|
||||
self.new_entry.get_text().encode("utf-8")).hexdigest()
|
||||
self.cfg.update("password", password, "login")
|
||||
logging.debug("Password changed successfully")
|
||||
self.close_window()
|
||||
|
||||
def on_type_password(self, entry):
|
||||
"""
|
||||
Validate the old & new password
|
||||
"""
|
||||
pwd = self.cfg.read("password", "login")
|
||||
old_is_ok = True
|
||||
if self.new_entry.get_text() != self.new2_entry.get_text():
|
||||
self.new_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY,
|
||||
"dialog-error-symbolic")
|
||||
self.new2_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY,
|
||||
"dialog-error-symbolic")
|
||||
are_diff = True
|
||||
elif len(self.new_entry.get_text()) == 0:
|
||||
are_diff = True
|
||||
elif len(self.new_entry.get_text()) == 0:
|
||||
are_diff = True
|
||||
else:
|
||||
are_diff = False
|
||||
if len(pwd) != 0:
|
||||
if sha256(self.old_entry.get_text().encode('utf-8')).hexdigest() != pwd:
|
||||
self.old_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY,
|
||||
"dialog-error-symbolic")
|
||||
old_is_ok = False
|
||||
else:
|
||||
old_is_ok = True
|
||||
if old_is_ok:
|
||||
self.old_entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, None)
|
||||
if not are_diff:
|
||||
self.new_entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, None)
|
||||
self.new2_entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, None)
|
||||
self.apply_button.set_sensitive(not are_diff and old_is_ok)
|
||||
|
||||
def generate_header_bar(self):
|
||||
"""
|
||||
Generate header bar box
|
||||
"""
|
||||
left_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
right_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
cancel_button = Gtk.Button.new_with_label(_("Cancel"))
|
||||
cancel_button.connect("clicked", self.close_window)
|
||||
cancel_button.get_style_context().add_class("destructive-action")
|
||||
left_box.add(cancel_button)
|
||||
|
||||
self.apply_button.get_style_context().add_class("suggested-action")
|
||||
self.apply_button.connect("clicked", self.update_password)
|
||||
self.apply_button.set_sensitive(False)
|
||||
right_box.add(self.apply_button)
|
||||
|
||||
self.hb.pack_start(left_box)
|
||||
self.hb.pack_end(right_box)
|
||||
self.set_titlebar(self.hb)
|
||||
|
||||
def close_window(self, *args):
|
||||
"""
|
||||
Close the window
|
||||
"""
|
||||
logging.debug("Closing PasswordWindow")
|
||||
self.destroy()
|
62
Authenticator/widgets/confirmation.py
Normal file
62
Authenticator/widgets/confirmation.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Gio, Gdk
|
||||
import logging
|
||||
|
||||
|
||||
class ConfirmationMessage(Gtk.Window):
|
||||
result = None
|
||||
|
||||
def __init__(self, parent, message):
|
||||
try:
|
||||
self.dialog = Gtk.MessageDialog(
|
||||
transient_for=parent,
|
||||
modal=True,
|
||||
destroy_with_parent=True,
|
||||
text=message,
|
||||
buttons=Gtk.ButtonsType.OK_CANCEL)
|
||||
logging.debug("Confirmation message created successfully")
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Show confirmation dialog
|
||||
"""
|
||||
try:
|
||||
self.result = self.dialog.run()
|
||||
self.dialog.hide()
|
||||
except AttributeError:
|
||||
logging.error("Confiramation message was not created correctly")
|
||||
return None
|
||||
|
||||
def get_confirmation(self):
|
||||
"""
|
||||
get confirmation
|
||||
"""
|
||||
return self.result == Gtk.ResponseType.OK
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
Destroy the message
|
||||
"""
|
||||
self.dialog.destroy()
|
233
Authenticator/widgets/headerbar.py
Normal file
233
Authenticator/widgets/headerbar.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio
|
||||
from Authenticator.utils import show_app_menu
|
||||
import logging
|
||||
from gettext import gettext as _
|
||||
from Authenticator.models.observer import Observer
|
||||
|
||||
|
||||
class HeaderBar(Gtk.HeaderBar, Observer):
|
||||
search_button = Gtk.ToggleButton()
|
||||
add_button = Gtk.Button()
|
||||
view_mode_button = Gtk.Button()
|
||||
settings_button = Gtk.Button()
|
||||
remove_button = Gtk.Button()
|
||||
cancel_button = Gtk.Button()
|
||||
select_button = Gtk.Button()
|
||||
lock_button = Gtk.Button()
|
||||
|
||||
popover = None
|
||||
|
||||
def __init__(self, app, window):
|
||||
self.app = app
|
||||
self.window = window
|
||||
Gtk.HeaderBar.__init__(self)
|
||||
self.generate()
|
||||
|
||||
def generate(self):
|
||||
self.set_show_close_button(True)
|
||||
right_box = self.generate_right_box()
|
||||
left_box = self.generate_left_box()
|
||||
|
||||
if show_app_menu():
|
||||
# add settings menu
|
||||
self.generate_popover(right_box)
|
||||
|
||||
self.pack_start(left_box)
|
||||
self.pack_end(right_box)
|
||||
self.refresh()
|
||||
|
||||
def generate_left_box(self):
|
||||
left_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
remove_icon = Gio.ThemedIcon(name="user-trash-symbolic")
|
||||
remove_image = Gtk.Image.new_from_gicon(
|
||||
remove_icon, Gtk.IconSize.BUTTON)
|
||||
self.remove_button.set_tooltip_text(_("Remove selected accounts"))
|
||||
self.remove_button.set_image(remove_image)
|
||||
self.remove_button.set_sensitive(False)
|
||||
|
||||
add_icon = Gio.ThemedIcon(name="list-add-symbolic")
|
||||
add_image = Gtk.Image.new_from_gicon(add_icon, Gtk.IconSize.BUTTON)
|
||||
self.add_button.set_tooltip_text(_("Add a new account"))
|
||||
self.add_button.set_image(add_image)
|
||||
|
||||
lock_icon = Gio.ThemedIcon(name="changes-prevent-symbolic")
|
||||
lock_image = Gtk.Image.new_from_gicon(lock_icon, Gtk.IconSize.BUTTON)
|
||||
self.lock_button.set_tooltip_text(_("Lock the Application"))
|
||||
self.lock_button.set_image(lock_image)
|
||||
|
||||
|
||||
left_box.add(self.remove_button)
|
||||
left_box.add(self.add_button)
|
||||
left_box.add(self.lock_button)
|
||||
return left_box
|
||||
|
||||
def generate_right_box(self):
|
||||
count = self.app.db.count()
|
||||
is_grid = self.app.cfg.read("view-mode", "preferences").lower() == "grid"
|
||||
right_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
select_icon = Gio.ThemedIcon(name="object-select-symbolic")
|
||||
select_image = Gtk.Image.new_from_gicon(
|
||||
select_icon, Gtk.IconSize.BUTTON)
|
||||
self.select_button.set_tooltip_text(_("Selection mode"))
|
||||
self.select_button.set_image(select_image)
|
||||
|
||||
search_icon = Gio.ThemedIcon(name="system-search-symbolic")
|
||||
search_image = Gtk.Image.new_from_gicon(
|
||||
search_icon, Gtk.IconSize.BUTTON)
|
||||
self.search_button.set_tooltip_text(_("Search"))
|
||||
self.search_button.set_image(search_image)
|
||||
self.search_button.set_visible(count > 0)
|
||||
|
||||
self.set_toggle_view_mode_icon(is_grid)
|
||||
self.view_mode_button.connect("clicked", self.toggle_view_mode)
|
||||
|
||||
self.cancel_button.set_label(_("Cancel"))
|
||||
|
||||
right_box.add(self.search_button)
|
||||
right_box.add(self.view_mode_button)
|
||||
right_box.add(self.select_button)
|
||||
right_box.add(self.cancel_button)
|
||||
return right_box
|
||||
|
||||
def generate_popover(self, box):
|
||||
settings_icon = Gio.ThemedIcon(name="open-menu-symbolic")
|
||||
settings_image = Gtk.Image.new_from_gicon(
|
||||
settings_icon, Gtk.IconSize.BUTTON)
|
||||
self.settings_button.set_tooltip_text(_("Settings"))
|
||||
self.settings_button.set_image(settings_image)
|
||||
self.settings_button.connect("clicked", self.toggle_popover)
|
||||
|
||||
self.popover = Gtk.Popover.new_from_model(
|
||||
self.settings_button, self.app.menu)
|
||||
self.popover.props.width_request = 200
|
||||
box.add(self.settings_button)
|
||||
|
||||
def toggle_popover(self, *args):
|
||||
if self.popover:
|
||||
if self.popover.get_visible():
|
||||
self.popover.hide()
|
||||
else:
|
||||
self.popover.show_all()
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
locked = kwargs.pop("locked", None)
|
||||
unlocked = kwargs.pop("unlocked", None)
|
||||
counter = kwargs.pop("counter", -1)
|
||||
if locked:
|
||||
self.view_mode_button.set_visible(False)
|
||||
self.select_button.set_visible(False)
|
||||
self.search_button.set_visible(False)
|
||||
self.add_button.set_visible(False)
|
||||
self.lock_button.set_visible(False)
|
||||
self.lock_button.set_no_show_all(True)
|
||||
self.view_mode_button.set_no_show_all(True)
|
||||
self.select_button.set_no_show_all(True)
|
||||
self.search_button.set_no_show_all(True)
|
||||
self.add_button.set_no_show_all(True)
|
||||
elif unlocked or counter >= 0:
|
||||
self.view_mode_button.set_visible(counter > 0)
|
||||
self.select_button.set_visible(counter > 0)
|
||||
self.search_button.set_visible(counter > 0)
|
||||
self.add_button.set_visible(True)
|
||||
self.view_mode_button.set_no_show_all(not counter > 0)
|
||||
self.select_button.set_no_show_all(not counter > 0)
|
||||
self.search_button.set_no_show_all(not counter > 0)
|
||||
self.add_button.set_no_show_all(False)
|
||||
if self.app.cfg.read("state", "login"):
|
||||
self.lock_button.set_visible(True)
|
||||
self.lock_button.set_no_show_all(False)
|
||||
else:
|
||||
self.lock_button.set_visible(False)
|
||||
self.lock_button.set_no_show_all(True)
|
||||
|
||||
def toggle_view_mode(self, *args):
|
||||
is_grid = self.app.cfg.read("view-mode", "preferences").lower() == "grid"
|
||||
if not is_grid:
|
||||
view_mode = "grid"
|
||||
self.app.cfg.update("view-mode", "grid" , "preferences")
|
||||
else:
|
||||
view_mode = "list"
|
||||
self.app.cfg.update("view-mode", "list" , "preferences")
|
||||
self.set_toggle_view_mode_icon(not is_grid)
|
||||
self.window.emit("view_mode_changed", view_mode)
|
||||
|
||||
def set_toggle_view_mode_icon(self, is_grid):
|
||||
if not is_grid:
|
||||
view_mode_icon = Gio.ThemedIcon(name="view-grid-symbolic")
|
||||
self.view_mode_button.set_tooltip_text(_("Grid mode"))
|
||||
else:
|
||||
view_mode_icon = Gio.ThemedIcon(name="view-list-symbolic")
|
||||
self.view_mode_button.set_tooltip_text(_("List mode"))
|
||||
view_mode_image = Gtk.Image.new_from_gicon(view_mode_icon, Gtk.IconSize.BUTTON)
|
||||
self.view_mode_button.set_image(view_mode_image)
|
||||
|
||||
def toggle_select_mode(self):
|
||||
is_select_mode = self.window.is_select_mode
|
||||
pass_enabled = self.app.cfg.read("state", "login")
|
||||
|
||||
self.remove_button.set_visible(is_select_mode)
|
||||
self.cancel_button.set_visible(is_select_mode)
|
||||
self.set_show_close_button(not is_select_mode)
|
||||
self.settings_button.set_visible(not is_select_mode)
|
||||
|
||||
self.lock_button.set_visible(not is_select_mode and pass_enabled)
|
||||
self.add_button.set_visible(not is_select_mode)
|
||||
self.select_button.set_visible(not is_select_mode)
|
||||
|
||||
if is_select_mode:
|
||||
self.get_style_context().add_class("selection-mode")
|
||||
else:
|
||||
self.get_style_context().remove_class("selection-mode")
|
||||
|
||||
def toggle_search(self):
|
||||
self.search_button.set_active(not self.search_button.get_active())
|
||||
|
||||
def toggle_settings_button(self, visible):
|
||||
if show_app_menu():
|
||||
self.settings_button.set_visible(visible)
|
||||
self.settings_button.set_no_show_all(not visible)
|
||||
|
||||
|
||||
def refresh(self):
|
||||
is_locked = self.app.locked
|
||||
pass_enabled = self.app.cfg.read("state", "login")
|
||||
can_be_locked = not is_locked and pass_enabled
|
||||
count = self.app.db.count()
|
||||
self.view_mode_button.set_visible(not count == 0 and not is_locked)
|
||||
self.select_button.set_visible(not count == 0 and not is_locked)
|
||||
self.search_button.set_visible(not count == 0 and not is_locked)
|
||||
self.view_mode_button.set_no_show_all(not (not count == 0 and not is_locked))
|
||||
self.select_button.set_no_show_all(not(not count == 0 and not is_locked))
|
||||
self.search_button.set_no_show_all(not(not count == 0 and not is_locked))
|
||||
self.lock_button.set_visible(can_be_locked)
|
||||
self.add_button.set_visible(not is_locked)
|
||||
self.lock_button.set_no_show_all(not can_be_locked)
|
||||
self.add_button.set_no_show_all(is_locked)
|
||||
|
||||
self.toggle_settings_button(True)
|
||||
self.cancel_button.set_visible(False)
|
||||
self.remove_button.set_visible(False)
|
||||
self.cancel_button.set_no_show_all(True)
|
||||
self.remove_button.set_no_show_all(True)
|
82
Authenticator/widgets/inapp_notification.py
Normal file
82
Authenticator/widgets/inapp_notification.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
from gettext import gettext as _
|
||||
import logging
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio, GLib
|
||||
|
||||
|
||||
class InAppNotification(Gtk.Revealer):
|
||||
timer = 0
|
||||
|
||||
def __init__(self, message="", undo_action=None, timeout=5):
|
||||
Gtk.Revealer.__init__(self)
|
||||
self.timeout = timeout
|
||||
self.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN)
|
||||
self.message = message
|
||||
self.undo_action = undo_action
|
||||
self.generate_components()
|
||||
GLib.timeout_add_seconds(1, self.update_timer)
|
||||
|
||||
def generate_components(self):
|
||||
self.get_style_context().add_class("top")
|
||||
self.infobar = Gtk.InfoBar()
|
||||
self.infobar.set_show_close_button(True)
|
||||
self.infobar.connect("response", self.response)
|
||||
self.infobar.set_default_response(Gtk.ResponseType.CLOSE)
|
||||
self.infobar.set_message_type(Gtk.MessageType.INFO)
|
||||
|
||||
content_area = self.infobar.get_content_area()
|
||||
action_area = self.infobar.get_action_area()
|
||||
self.undo_button = None
|
||||
if self.undo_action:
|
||||
self.undo_button = self.infobar.add_button(
|
||||
_("Undo"), Gtk.ResponseType.CANCEL)
|
||||
|
||||
self.message_label = Gtk.Label()
|
||||
self.message_label.set_text(self.message)
|
||||
|
||||
content_area.add(self.message_label)
|
||||
self.add(self.infobar)
|
||||
|
||||
def set_message_type(self, message_type):
|
||||
self.infobar.set_message_type(message_type)
|
||||
|
||||
def on_hide_notification(self, *args):
|
||||
self.hide()
|
||||
|
||||
def show(self):
|
||||
self.set_reveal_child(True)
|
||||
|
||||
def hide(self):
|
||||
self.set_reveal_child(False)
|
||||
|
||||
def update(self, message, undo_action=None):
|
||||
self.message_label.set_text(message)
|
||||
self.timer = 0
|
||||
if undo_action:
|
||||
if not self.undo_button:
|
||||
self.undo_button = self.infobar.add_button(
|
||||
_("Undo"), Gtk.ResponseType.CANCEL)
|
||||
else:
|
||||
self.undo_button.set_visible(True)
|
||||
self.undo_button.set_no_show_all(False)
|
||||
self.undo_action = undo_action
|
||||
else:
|
||||
if self.undo_button:
|
||||
self.undo_button.set_visible(False)
|
||||
self.undo_button.set_no_show_all(True)
|
||||
|
||||
def response(self, infobar, response_id):
|
||||
if response_id == Gtk.ResponseType.CLOSE:
|
||||
self.hide()
|
||||
elif response_id == Gtk.ResponseType.CANCEL:
|
||||
self.undo_action()
|
||||
|
||||
def update_timer(self):
|
||||
if self.get_reveal_child():
|
||||
if self.timer == self.timeout:
|
||||
self.hide()
|
||||
self.timer = 0
|
||||
else:
|
||||
self.timer += 1
|
||||
return True
|
118
Authenticator/widgets/login_window.py
Normal file
118
Authenticator/widgets/login_window.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
from gettext import gettext as _
|
||||
from Authenticator.models.observer import Observer
|
||||
|
||||
class LoginWindow(Gtk.Box, Observer):
|
||||
password_entry = None
|
||||
unlock_button = None
|
||||
|
||||
def __init__(self, application, window):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
||||
self.app = application
|
||||
self.window = window
|
||||
self.password_entry = Gtk.Entry()
|
||||
self.unlock_button = Gtk.Button()
|
||||
self.generate()
|
||||
self.window.connect("key-press-event", self.__on_key_press)
|
||||
|
||||
def generate(self):
|
||||
password_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
self.password_entry.set_visibility(False)
|
||||
self.password_entry.set_placeholder_text(_("Enter your password"))
|
||||
password_box.pack_start(self.password_entry, False, False, 6)
|
||||
|
||||
self.unlock_button.set_label(_("Unlock"))
|
||||
self.unlock_button.connect("clicked", self.on_unlock)
|
||||
|
||||
password_box.pack_start(self.unlock_button, False, False, 6)
|
||||
self.pack_start(password_box, True, False, 6)
|
||||
|
||||
def on_unlock(self, *args):
|
||||
"""
|
||||
Password check and unlock
|
||||
"""
|
||||
typed_pass = self.password_entry.get_text()
|
||||
ecrypted_pass = sha256(typed_pass.encode("utf-8")).hexdigest()
|
||||
login_pass = self.app.cfg.read("password", "login")
|
||||
if ecrypted_pass == login_pass or login_pass == typed_pass == "":
|
||||
self.password_entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, None)
|
||||
self.toggle_lock()
|
||||
self.password_entry.set_text("")
|
||||
else:
|
||||
self.password_entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, "dialog-error-symbolic")
|
||||
|
||||
def __on_key_press(self, widget, event):
|
||||
keyname = Gdk.keyval_name(event.keyval).lower()
|
||||
if self.window.is_locked():
|
||||
if keyname == "return":
|
||||
self.on_unlock()
|
||||
return True
|
||||
else:
|
||||
pass_enabled = self.app.cfg.read("state", "login")
|
||||
if keyname == "l" and pass_enabled:
|
||||
if event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
self.toggle_lock()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def toggle_lock(self, *args):
|
||||
"""
|
||||
Lock/unlock the application
|
||||
"""
|
||||
pass_enabled = self.app.cfg.read("state", "login")
|
||||
if pass_enabled:
|
||||
self.app.locked = not self.app.locked
|
||||
self.window.counter = 0
|
||||
if self.app.locked:
|
||||
self.focus()
|
||||
self.window.emit("locked", True)
|
||||
else:
|
||||
self.window.emit("unlocked", True)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
is_locked = kwargs.pop("locked", None)
|
||||
is_unlocked = kwargs.pop("unlocked", None)
|
||||
if is_locked:
|
||||
self.show()
|
||||
elif is_unlocked:
|
||||
self.hide()
|
||||
|
||||
def toggle(self, visible):
|
||||
self.set_visible(visible)
|
||||
self.set_no_show_all(not visible)
|
||||
|
||||
def hide(self):
|
||||
self.toggle(False)
|
||||
|
||||
def show(self):
|
||||
self.toggle(True)
|
||||
|
||||
def focus(self):
|
||||
self.password_entry.grab_focus_without_selecting()
|
64
Authenticator/widgets/no_account_window.py
Normal file
64
Authenticator/widgets/no_account_window.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
import logging
|
||||
from gettext import gettext as _
|
||||
from Authenticator.models.observer import Observer
|
||||
|
||||
class NoAccountWindow(Gtk.Box, Observer):
|
||||
|
||||
def __init__(self):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL,
|
||||
spacing=6)
|
||||
self.generate()
|
||||
|
||||
def generate(self):
|
||||
logo_image = Gtk.Image()
|
||||
logo_image.set_from_icon_name("dialog-information-symbolic",
|
||||
Gtk.IconSize.DIALOG)
|
||||
no_apps_label = Gtk.Label()
|
||||
no_apps_label.set_text(_("There's no account at the moment"))
|
||||
|
||||
self.pack_start(logo_image, False, False, 6)
|
||||
self.pack_start(no_apps_label, False, False, 6)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
unlocked = kwargs.pop("unlocked", None)
|
||||
locked = kwargs.pop("locked", None)
|
||||
counter = kwargs.pop("counter", None)
|
||||
if counter != 0 or locked:
|
||||
self.hide()
|
||||
elif unlocked or counter == 0:
|
||||
self.show()
|
||||
|
||||
def toggle(self, visible):
|
||||
self.set_visible(visible)
|
||||
self.set_no_show_all(not visible)
|
||||
|
||||
def is_visible(self):
|
||||
return self.get_visible()
|
||||
|
||||
def hide(self):
|
||||
self.toggle(False)
|
||||
|
||||
def show(self):
|
||||
self.toggle(True)
|
107
Authenticator/widgets/search_bar.py
Normal file
107
Authenticator/widgets/search_bar.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio, Gdk
|
||||
import logging
|
||||
|
||||
|
||||
class SearchBar(Gtk.Revealer):
|
||||
|
||||
def __init__(self, window, search_button, *args):
|
||||
self.search_entry = Gtk.SearchEntry()
|
||||
self.search_list = args[0]
|
||||
self.search_button = search_button
|
||||
self.window = window
|
||||
self.generate()
|
||||
self.search_button.connect("toggled", self.toggle)
|
||||
self.window.connect("key-press-event", self.__on_key_press)
|
||||
|
||||
def generate(self):
|
||||
Gtk.Revealer.__init__(self)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
self.search_entry.set_width_chars(28)
|
||||
self.search_entry.connect("search-changed", self.filter_applications)
|
||||
|
||||
box.pack_start(self.search_entry, True, False, 12)
|
||||
box.props.margin = 6
|
||||
|
||||
self.add(box)
|
||||
self.set_reveal_child(False)
|
||||
|
||||
def toggle(self, *args):
|
||||
if self.is_visible():
|
||||
self.search_entry.set_text("")
|
||||
self.set_reveal_child(False)
|
||||
for search_list in self.search_list:
|
||||
search_list.set_filter_func(lambda x, y, z: True, None, False)
|
||||
else:
|
||||
self.set_reveal_child(True)
|
||||
self.focus()
|
||||
|
||||
def filter_func(self, row, data, notify_destroy):
|
||||
"""
|
||||
Filter function, used to check if the entered data exists on the application ListBox
|
||||
"""
|
||||
if isinstance(row, Gtk.FlowBoxChild):
|
||||
row = row.get_children()[0]
|
||||
app_label = row.get_name()
|
||||
data = data.lower()
|
||||
if len(data) > 0:
|
||||
return data in app_label.lower()
|
||||
else:
|
||||
return True
|
||||
|
||||
def __on_key_press(self, widget, event):
|
||||
keyname = Gdk.keyval_name(event.keyval).lower()
|
||||
if keyname == 'escape' and self.search_button.get_active():
|
||||
if self.search_entry.is_focus():
|
||||
self.search_button.set_active(False)
|
||||
self.search_entry.set_text("")
|
||||
else:
|
||||
self.focus()
|
||||
|
||||
if not "is_locked" in dir(self.window) or not self.window.is_locked():
|
||||
if keyname == "backspace":
|
||||
if self.is_empty() and self.is_visible():
|
||||
self.search_button.set_active(False)
|
||||
return True
|
||||
|
||||
if event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
if keyname == 'f':
|
||||
self.search_button.set_active(
|
||||
not self.search_button.get_active())
|
||||
return True
|
||||
return False
|
||||
|
||||
def focus(self):
|
||||
self.search_entry.grab_focus_without_selecting()
|
||||
|
||||
def is_visible(self):
|
||||
return self.get_reveal_child()
|
||||
|
||||
def is_empty(self):
|
||||
return len(self.search_entry.get_text()) == 0
|
||||
|
||||
def filter_applications(self, entry):
|
||||
data = entry.get_text().strip()
|
||||
for search_list in self.search_list:
|
||||
search_list.set_filter_func(self.filter_func, data, False)
|
190
Authenticator/widgets/settings.py
Normal file
190
Authenticator/widgets/settings.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
from Authenticator.models.settings import SettingsReader
|
||||
from Authenticator.widgets.change_password import PasswordWindow
|
||||
from gettext import gettext as _
|
||||
import logging
|
||||
|
||||
class SettingsWindow(Gtk.Window):
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.cfg = SettingsReader()
|
||||
self.auto_lock_time = Gtk.SpinButton()
|
||||
self.enable_switch = Gtk.CheckButton()
|
||||
self.auto_lock_switch = Gtk.CheckButton()
|
||||
self.password_button = Gtk.Button()
|
||||
self.hb = Gtk.HeaderBar()
|
||||
self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.generate_window()
|
||||
self.generate_components()
|
||||
|
||||
def generate_window(self):
|
||||
Gtk.Window.__init__(self, title=_("Settings"), type=Gtk.WindowType.TOPLEVEL,
|
||||
destroy_with_parent=True, modal=True)
|
||||
self.connect("delete-event", self.close_window)
|
||||
self.resize(400, 300)
|
||||
self.set_size_request(400, 300)
|
||||
self.set_border_width(18)
|
||||
self.set_resizable(False)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||
self.set_transient_for(self.parent)
|
||||
self.connect("key_press_event", self.on_key_press)
|
||||
self.hb.set_show_close_button(True)
|
||||
self.set_titlebar(self.hb)
|
||||
self.add(self.main_box)
|
||||
|
||||
|
||||
def show_window(self):
|
||||
self.show_all()
|
||||
|
||||
def on_key_press(self, key, key_event):
|
||||
"""
|
||||
Keyboard Listener handler
|
||||
"""
|
||||
if Gdk.keyval_name(key_event.keyval) == "Escape":
|
||||
self.close_window()
|
||||
|
||||
def generate_components(self):
|
||||
"""
|
||||
Generate all the components
|
||||
"""
|
||||
self.stack = Gtk.Stack()
|
||||
self.stack.set_vexpand(True)
|
||||
self.stack.set_hexpand(True)
|
||||
self.stack.set_transition_type(
|
||||
Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||
self.stack.set_transition_duration(1000)
|
||||
|
||||
stack_switcher = Gtk.StackSwitcher()
|
||||
stack_switcher.set_stack(self.stack)
|
||||
self.hb.set_custom_title(stack_switcher)
|
||||
|
||||
behavior_settings = self.generate_behavior_settings()
|
||||
account_settings = self.generate_account_settings()
|
||||
self.stack.add_titled(behavior_settings, "behavior", _("Behavior"))
|
||||
self.stack.add_titled(account_settings, "account", _("Account"))
|
||||
self.main_box.add(self.stack)
|
||||
|
||||
def generate_account_settings(self):
|
||||
"""
|
||||
Create a box with login settings components
|
||||
:return (Gtk.Box): Box contains all the components
|
||||
"""
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
password_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
lock_enabled = bool(self.cfg.read("state", "login"))
|
||||
self.enable_switch.set_active(lock_enabled)
|
||||
self.enable_switch.connect("toggled", self.on_switch_activated)
|
||||
|
||||
password_label = Gtk.Label()
|
||||
password_label.set_label(_("Password protection"))
|
||||
|
||||
self.password_button.get_style_context().add_class("flat")
|
||||
self.password_button.get_style_context().add_class("text-button")
|
||||
self.password_button.set_label("******")
|
||||
self.password_button.connect("clicked", self.new_password_window)
|
||||
self.password_button.set_sensitive(lock_enabled)
|
||||
|
||||
password_box.pack_start(self.enable_switch, False, True, 6)
|
||||
password_box.pack_start(password_label, False, True, 6)
|
||||
password_box.pack_start(self.password_button, False, True, 6)
|
||||
|
||||
main_box.pack_start(password_box, False, True, 6)
|
||||
return main_box
|
||||
|
||||
def generate_behavior_settings(self):
|
||||
"""
|
||||
Create a box with user settings components
|
||||
:return (Gtk.Box): Box contains all the components
|
||||
"""
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
is_auto_lock_active = bool(self.cfg.read("auto-lock", "preferences"))
|
||||
auto_lock_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
auto_lock_label = Gtk.Label().new(_("Auto-lock the application (m):"))
|
||||
self.auto_lock_switch.set_sensitive(self.cfg.read("state", "login"))
|
||||
self.auto_lock_switch.set_active(is_auto_lock_active)
|
||||
self.auto_lock_switch.connect("toggled", self.on_auto_lock_activated)
|
||||
|
||||
default_value = self.cfg.read("auto-lock-time", "preferences")
|
||||
if default_value < 1 or default_value > 10:
|
||||
default_value = 3
|
||||
adjustment = Gtk.Adjustment(value=default_value, lower=1, upper=10,
|
||||
step_increment=1, page_increment=1, page_size=0)
|
||||
self.auto_lock_time.connect(
|
||||
"value-changed", self.on_auto_lock_time_changed)
|
||||
self.auto_lock_time.set_adjustment(adjustment)
|
||||
self.auto_lock_time.set_sensitive(is_auto_lock_active)
|
||||
self.auto_lock_time.set_value(default_value)
|
||||
|
||||
auto_lock_box.pack_start(self.auto_lock_switch, False, True, 6)
|
||||
auto_lock_box.pack_start(auto_lock_label, False, True, 6)
|
||||
auto_lock_box.pack_start(self.auto_lock_time, False, True, 12)
|
||||
|
||||
main_box.pack_start(auto_lock_box, False, True, 6)
|
||||
return main_box
|
||||
|
||||
def new_password_window(self, *args):
|
||||
"""
|
||||
Show a new password window
|
||||
"""
|
||||
pass_window = PasswordWindow(self)
|
||||
pass_window.show_window()
|
||||
|
||||
def on_auto_lock_time_changed(self, spin_button):
|
||||
"""
|
||||
Update auto lock time
|
||||
"""
|
||||
self.cfg.update("auto-lock-time",
|
||||
spin_button.get_value_as_int(), "preferences")
|
||||
logging.info("Auto lock time updated")
|
||||
|
||||
def on_switch_activated(self, switch, *args):
|
||||
"""
|
||||
Update password state : enabled/disabled
|
||||
"""
|
||||
self.password_button.set_sensitive(switch.get_active())
|
||||
self.cfg.update("state", switch.get_active(), "login")
|
||||
if switch.get_active():
|
||||
password = self.cfg.read("password", "login")
|
||||
if len(password) == 0:
|
||||
self.new_password_window()
|
||||
self.auto_lock_switch.set_sensitive(switch.get_active())
|
||||
logging.info("Password enabled/disabled")
|
||||
self.parent.refresh_window()
|
||||
|
||||
def on_auto_lock_activated(self, switch, *args):
|
||||
"""
|
||||
Update auto-lock state : enabled/disabled
|
||||
"""
|
||||
self.auto_lock_time.set_sensitive(switch.get_active())
|
||||
self.cfg.update("auto-lock", switch.get_active(), "preferences")
|
||||
logging.info("Auto lock state updated")
|
||||
|
||||
def close_window(self, *args):
|
||||
"""
|
||||
Close the window
|
||||
"""
|
||||
logging.debug("SettingsWindow closed")
|
||||
self.destroy()
|
222
Authenticator/widgets/window.py
Normal file
222
Authenticator/widgets/window.py
Normal file
|
@ -0,0 +1,222 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
|
||||
from Authenticator.widgets.add_account import AddAccount
|
||||
from Authenticator.widgets.accounts_window import AccountsWindow
|
||||
from Authenticator.widgets.login_window import LoginWindow
|
||||
from Authenticator.widgets.no_account_window import NoAccountWindow
|
||||
from Authenticator.widgets.headerbar import HeaderBar
|
||||
from Authenticator.widgets.inapp_notification import InAppNotification
|
||||
from Authenticator.interfaces.observable import Observable
|
||||
from hashlib import sha256
|
||||
from gettext import gettext as _
|
||||
import logging
|
||||
|
||||
|
||||
class Window(Gtk.ApplicationWindow, GObject.GObject):
|
||||
__gsignals__ = {
|
||||
'changed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
|
||||
'locked': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
|
||||
'unlocked': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
|
||||
'view_mode_changed': (GObject.SignalFlags.RUN_LAST, None, (str,))
|
||||
}
|
||||
counter = 0
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
is_select_mode = False
|
||||
|
||||
def __init__(self, application):
|
||||
self.app = application
|
||||
self.observable = Observable()
|
||||
self.generate_window()
|
||||
self.generate_header_bar()
|
||||
self.generate_accounts_box()
|
||||
self.generate_no_accounts_box()
|
||||
self.generate_login_box()
|
||||
if self.app.locked:
|
||||
self.emit("locked", True)
|
||||
else:
|
||||
self.emit("unlocked", True)
|
||||
|
||||
GLib.timeout_add_seconds(60, self.refresh_counter)
|
||||
|
||||
def generate_window(self, *args):
|
||||
"""
|
||||
Generate application window (Gtk.Window)
|
||||
"""
|
||||
Gtk.ApplicationWindow.__init__(self, type=Gtk.WindowType.TOPLEVEL,
|
||||
application=self.app)
|
||||
self.move_latest_position()
|
||||
self.set_wmclass("org.gnome.Authenticator", "Gnome Authenticator")
|
||||
self.set_icon_name("org.gnome.Authenticator")
|
||||
self.use_latest_size()
|
||||
self.set_resizable(True)
|
||||
self.connect("key_press_event", self.on_key_press)
|
||||
self.connect("delete-event", lambda x, y: self.app.on_quit())
|
||||
self.notification = InAppNotification()
|
||||
self.main_box.pack_start(self.notification, False, False, 0)
|
||||
self.add(self.main_box)
|
||||
|
||||
def on_key_press(self, app, key_event):
|
||||
"""
|
||||
Keyboard Listener handling
|
||||
"""
|
||||
keyname = Gdk.keyval_name(key_event.keyval).lower()
|
||||
if not self.is_locked():
|
||||
|
||||
if not self.no_account_box.is_visible():
|
||||
if keyname == "s" or keyname == "escape":
|
||||
if key_event.state == Gdk.ModifierType.CONTROL_MASK or not self.hb.select_button.get_visible():
|
||||
self.toggle_select()
|
||||
return True
|
||||
|
||||
if keyname == "n":
|
||||
if key_event.state == Gdk.ModifierType.CONTROL_MASK:
|
||||
self.add_account()
|
||||
return True
|
||||
if keyname == "m":
|
||||
if key_event.state == Gdk.ModifierType.CONTROL_MASK:
|
||||
self.hb.toggle_view_mode()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def refresh_counter(self):
|
||||
"""
|
||||
Add a value to the counter each 60 seconds
|
||||
"""
|
||||
if not self.app.locked:
|
||||
self.counter += 1
|
||||
if self.app.cfg.read("auto-lock", "preferences"):
|
||||
if self.counter == self.app.cfg.read("auto-lock-time", "preferences") - 1:
|
||||
self.counter = 0
|
||||
self.emit("locked", True)
|
||||
return True
|
||||
|
||||
def generate_login_box(self):
|
||||
"""
|
||||
Generate login form
|
||||
"""
|
||||
self.login_box = LoginWindow(self.app, self)
|
||||
self.observable.register(self.login_box)
|
||||
self.hb.lock_button.connect("clicked", lambda x : self.emit("locked", True))
|
||||
self.main_box.pack_start(self.login_box, True, False, 0)
|
||||
|
||||
def generate_accounts_box(self):
|
||||
self.accounts_box = AccountsWindow(self.app, self)
|
||||
self.observable.register(self.accounts_box)
|
||||
self.accounts_list = self.accounts_box.get_accounts_list()
|
||||
self.accounts_grid = self.accounts_box.get_accounts_grid()
|
||||
self.hb.remove_button.connect(
|
||||
"clicked", self.accounts_list.remove_selected)
|
||||
self.search_bar = self.accounts_box.get_search_bar()
|
||||
self.main_box.pack_start(self.accounts_box, True, True, 0)
|
||||
|
||||
def generate_header_bar(self):
|
||||
"""
|
||||
Generate a header bar box
|
||||
"""
|
||||
self.hb = HeaderBar(self.app, self)
|
||||
self.observable.register(self.hb)
|
||||
# connect signals
|
||||
self.hb.cancel_button.connect("clicked", self.toggle_select)
|
||||
self.hb.select_button.connect("clicked", self.toggle_select)
|
||||
self.hb.add_button.connect("clicked", self.add_account)
|
||||
self.set_titlebar(self.hb)
|
||||
|
||||
def add_account(self, *args):
|
||||
"""
|
||||
Create add application window
|
||||
"""
|
||||
add_account = AddAccount(self)
|
||||
add_account.show_window()
|
||||
|
||||
def toggle_view_mode(self, is_grid):
|
||||
|
||||
self.accounts_box.set_mode_view(is_grid)
|
||||
|
||||
def toggle_select(self, *args):
|
||||
"""
|
||||
Toggle select mode
|
||||
"""
|
||||
self.is_select_mode = not self.is_select_mode
|
||||
self.hb.toggle_select_mode()
|
||||
self.accounts_list.toggle_select_mode()
|
||||
self.accounts_grid.toggle_select_mode()
|
||||
|
||||
def generate_no_accounts_box(self):
|
||||
"""
|
||||
Generate a box with no accounts message
|
||||
"""
|
||||
self.no_account_box = NoAccountWindow()
|
||||
self.observable.register(self.no_account_box)
|
||||
self.main_box.pack_start(self.no_account_box, True, False, 0)
|
||||
|
||||
def do_view_mode_changed(self, *args):
|
||||
self.observable.update_observers(view_mode=args[0])
|
||||
|
||||
def do_changed(self, *args):
|
||||
counter = self.app.db.count()
|
||||
self.observable.update_observers(counter=counter)
|
||||
self.main_box.show_all()
|
||||
|
||||
def do_locked(self, *args):
|
||||
self.app.locked = True
|
||||
self.app.refresh_menu()
|
||||
self.observable.update_observers(locked=True)
|
||||
|
||||
def do_unlocked(self, *args):
|
||||
self.app.locked = False
|
||||
self.app.refresh_menu()
|
||||
counter = self.app.db.count()
|
||||
self.observable.update_observers(unlocked=True, counter=counter)
|
||||
self.main_box.show_all()
|
||||
|
||||
def is_locked(self):
|
||||
return self.app.locked
|
||||
|
||||
def save_window_state(self):
|
||||
"""
|
||||
Save window position
|
||||
"""
|
||||
pos_x, pos_y = self.get_position()
|
||||
size_x, size_y = self.get_size()
|
||||
self.app.cfg.update("position-x", pos_x, "preferences")
|
||||
self.app.cfg.update("position-y", pos_y, "preferences")
|
||||
self.app.cfg.update("size-x", size_x, "preferences")
|
||||
self.app.cfg.update("size-y", size_y, "preferences")
|
||||
|
||||
def move_latest_position(self):
|
||||
"""
|
||||
move the application window to the latest position
|
||||
"""
|
||||
x = self.app.cfg.read("position-x", "preferences")
|
||||
y = self.app.cfg.read("position-y", "preferences")
|
||||
if x != 0 and y != 0:
|
||||
self.move(x, y)
|
||||
else:
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
|
||||
def use_latest_size(self):
|
||||
x = self.app.cfg.read("size-x", "preferences")
|
||||
y = self.app.cfg.read("size-y", "preferences")
|
||||
self.resize(x, y)
|
||||
self.props.width_request = 500
|
BIN
data/icons/hicolor/16x16/apps/org.gnome.Authenticator.png
Normal file
BIN
data/icons/hicolor/16x16/apps/org.gnome.Authenticator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
data/icons/hicolor/22x22/apps/org.gnome.Authenticator.png
Normal file
BIN
data/icons/hicolor/22x22/apps/org.gnome.Authenticator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
data/icons/hicolor/256x256/apps/org.gnome.Authenticator.png
Normal file
BIN
data/icons/hicolor/256x256/apps/org.gnome.Authenticator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
data/icons/hicolor/32x32/apps/org.gnome.Authenticator.png
Normal file
BIN
data/icons/hicolor/32x32/apps/org.gnome.Authenticator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
data/icons/hicolor/48x48/apps/org.gnome.Authenticator.png
Normal file
BIN
data/icons/hicolor/48x48/apps/org.gnome.Authenticator.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 79 KiB |
39
data/org.gnome.Authenticator-post3.20.css
Normal file
39
data/org.gnome.Authenticator-post3.20.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
.applications-list {
|
||||
background-color: @theme_bg_color;
|
||||
}
|
||||
.application-list-row:selected GtkImage {
|
||||
color: @theme_fg_selected_color;
|
||||
}
|
||||
.application-name,
|
||||
.application-name-grid {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.application-logo-add {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.account-secret-code,
|
||||
.account-timer {
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.account-secret-code-grid,
|
||||
.account-timer-grid {
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.application-secret-code-select-mode {
|
||||
margin-left: 98px;
|
||||
}
|
||||
.choose-popover GtkLabel {
|
||||
padding: 10px;
|
||||
}
|
||||
|
39
data/org.gnome.Authenticator-pre3.20.css
Normal file
39
data/org.gnome.Authenticator-pre3.20.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
.applications-list {
|
||||
background-color: @theme_bg_color;
|
||||
}
|
||||
.application-list-row:selected GtkImage {
|
||||
color: @theme_fg_selected_color;
|
||||
}
|
||||
.application-name,
|
||||
.application-name-grid {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.application-logo-add {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.account-secret-code,
|
||||
.account-timer {
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.account-secret-code-grid,
|
||||
.account-timer-grid {
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.application-secret-code-select-mode {
|
||||
margin-left: 98px;
|
||||
}
|
||||
.choose-popover GtkLabel {
|
||||
padding: 10px;
|
||||
}
|
||||
|
17
data/org.gnome.Authenticator.appdata.xml.in
Normal file
17
data/org.gnome.Authenticator.appdata.xml.in
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<id type="desktop">org.gnome.Authenticator.desktop</id>
|
||||
<licence>GPL3</licence>
|
||||
<_name>Gnome Authenticator</_name>
|
||||
<_summary>Two Factor Authentication code generator</_summary>
|
||||
<description>
|
||||
<_p>Simple application that generates 2 factor authentication code, created for Gnome</_p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default" width="350" height="500">https://raw.githubusercontent.com/bil-elmoussaoui/Gnome-Authenticator/master/screenshots/screenshot1.png</screenshot>
|
||||
<screenshot width="350" height="500">https://raw.githubusercontent.com/bil-elmoussaoui/Gnome-Authenticator/master/screenshots/screenshot2.png</screenshot>
|
||||
<screenshot width="350" height="500">https://raw.githubusercontent.com/bil-elmoussaoui/Gnome-Authenticator/master/screenshots/screenshot7.png</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">https://github.com/bil-elmoussaoui/Gnome-Authenticator</url>
|
||||
<updatecontact>bil.elmoussaoui@gmail.com</updatecontact>
|
||||
</application>
|
19
data/org.gnome.Authenticator.desktop.in
Normal file
19
data/org.gnome.Authenticator.desktop.in
Normal file
|
@ -0,0 +1,19 @@
|
|||
[Desktop Entry]
|
||||
_Name=Gnome Authenticator
|
||||
_GenericName=Two Factor Authentication
|
||||
_Comment=Two-Factor Authentication code generator
|
||||
Type=Application
|
||||
Exec=gnome-authenticator
|
||||
Terminal=false
|
||||
Categories=GNOME;GTK;
|
||||
Keywords=Gnome;GTK;Verification;
|
||||
Icon=gnome-twofactorauth
|
||||
StartupNotify=true
|
||||
X-GNOME-Gettext-Domain=Authenticator
|
||||
StartupWMClass=org.gnome.Authenticator
|
||||
Actions=AboutDialog;
|
||||
|
||||
|
||||
[Desktop Action AboutDialog]
|
||||
Exec=gnome-authenticator --about
|
||||
_Name=About Gnome Authenticator
|
9
data/org.gnome.Authenticator.gresource.xml
Normal file
9
data/org.gnome.Authenticator.gresource.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/gnome/Authenticator">
|
||||
<file>org.gnome.Authenticator-post3.20.css</file>
|
||||
<file>org.gnome.Authenticator-pre3.20.css</file>
|
||||
<file preprocess="xml-stripblanks">about.ui</file>
|
||||
<file preprocess="xml-stripblanks">shortcuts.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
85
data/org.gnome.Authenticator.gschema.xml
Normal file
85
data/org.gnome.Authenticator.gschema.xml
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist>
|
||||
<schema path="/org/gnome/Authenticator/preferences/" id="org.gnome.Authenticator.preferences" gettext-domain="Authenticator">
|
||||
<key name="refresh-time" type="i">
|
||||
<default>30</default>
|
||||
<summary>Time to generate an other password</summary>
|
||||
<description>
|
||||
Time on seconds to generate a new password
|
||||
</description>
|
||||
</key>
|
||||
<key name="auto-lock" type="b">
|
||||
<default>false</default>
|
||||
<summary>Auto lock the application</summary>
|
||||
<description>
|
||||
Auto lock the application after a (auto-lock-time)s
|
||||
</description>
|
||||
</key>
|
||||
<key name="auto-lock-time" type="i">
|
||||
<default>3</default>
|
||||
<summary>Auto lock the application after x seconds</summary>
|
||||
<description>
|
||||
Auto lock the application after a x seconds
|
||||
</description>
|
||||
</key>
|
||||
<key name="position-x" type="i">
|
||||
<default>0</default>
|
||||
<summary>Default window postiton on x axes</summary>
|
||||
<description>
|
||||
Default window postiton on x axes
|
||||
</description>
|
||||
</key>
|
||||
<key name="position-y" type="i">
|
||||
<default>0</default>
|
||||
<summary>Default window postiton on y axes</summary>
|
||||
<description>
|
||||
Default window postiton on y axes
|
||||
</description>
|
||||
</key>
|
||||
|
||||
<key name="size-x" type="i">
|
||||
<default>500</default>
|
||||
<summary>Default window postiton on x axes</summary>
|
||||
<description>
|
||||
Default window postiton on x axes
|
||||
</description>
|
||||
</key>
|
||||
<key name="size-y" type="i">
|
||||
<default>650</default>
|
||||
<summary>Default window postiton on y axes</summary>
|
||||
<description>
|
||||
Default window postiton on y axes
|
||||
</description>
|
||||
</key>
|
||||
<key name="view-mode" type="s">
|
||||
<default>"list"</default>
|
||||
<summary>Default view mode grid/list</summary>
|
||||
<description>
|
||||
The way the generated codes will be shown list/grid mode
|
||||
</description>
|
||||
</key>
|
||||
<key name="night-mode" type="b">
|
||||
<default>false</default>
|
||||
<summary>Night mode</summary>
|
||||
<description>
|
||||
Enable/disable night mode within the application
|
||||
</description>
|
||||
</key>
|
||||
</schema>
|
||||
<schema path="/org/gnome/Authenticator/login/" id="org.gnome.Authenticator.login" gettext-domain="Authenticator">
|
||||
<key name="password" type="s">
|
||||
<default>""</default>
|
||||
<summary>Access code to user interface</summary>
|
||||
<description>
|
||||
Password to secure generated codes
|
||||
</description>
|
||||
</key>
|
||||
<key name="state" type="b">
|
||||
<default>false</default>
|
||||
<summary>Use password to access to user interface</summary>
|
||||
<description>
|
||||
Show a password form whenever the application is opened
|
||||
</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
103
flatpak/org.gnome.Authenticator.json
Normal file
103
flatpak/org.gnome.Authenticator.json
Normal file
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"app-id":"org.gnome.Authenticator",
|
||||
"branch":"0.1.1",
|
||||
"runtime":"org.gnome.Platform",
|
||||
"runtime-version":"3.22",
|
||||
"sdk":"org.gnome.Sdk",
|
||||
"command":"gnome-authenticator",
|
||||
"copy-icon":true,
|
||||
"separate-locales":false,
|
||||
"no-debuginfo":true,
|
||||
"rename-appdata-file":"org.gnome.Authenticator.appdata.xml",
|
||||
"rename-desktop-file":"org.gnome.Authenticator.desktop",
|
||||
"desktop-file-name-prefix":"",
|
||||
"finish-args":[
|
||||
"--socket=x11",
|
||||
"--filesystem=host",
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
"--talk-name=org.freedesktop.secrets",
|
||||
"--talk-name=ca.desrt.dconf",
|
||||
"--own-name=org.gnome.TwoFactorAuth"
|
||||
],
|
||||
"modules":[
|
||||
{
|
||||
"name":"zbar",
|
||||
"config-opts":[
|
||||
"--prefix=/usr",
|
||||
"--without-qt",
|
||||
"--without-gtk",
|
||||
"--disable-video",
|
||||
"CFLAGS=-DNDEBUG"
|
||||
],
|
||||
"sources":[
|
||||
{
|
||||
"type":"git",
|
||||
"url":"https://github.com/ZBar/ZBar",
|
||||
"branch":"master"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"pyotp",
|
||||
"sources":[
|
||||
{
|
||||
"type":"archive",
|
||||
"url":"https://pypi.python.org/packages/ac/0c/bd96508e36956ae627e527a7a7fba486865a738b4682e7290cd0e7c34f52/pyotp-2.2.4.tar.gz#md5=16cb1a08d38777ca74b5e9c7803810b6",
|
||||
"sha256":"92c3973ba91273e7e4a7fd4a1020ae4b050ccd2e149b554911e1b45ca458ac2d"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"pyaml",
|
||||
"sources":[
|
||||
{
|
||||
"type":"archive",
|
||||
"url":"https://pypi.python.org/packages/aa/bc/68c34bd6c5a7bd6d2ecf94ba7cd2337c9f9be58d670e2edef16fa1e0d6a2/pyaml-16.12.2.tar.gz#md5=3f2f2f113b65be2e33272ffff411ecc2",
|
||||
"sha256":"b865e4f53a85f4d8a092e7701f759a3237fb3ee8a928627401914aafadc00907"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"Pillow",
|
||||
"sources":[
|
||||
{
|
||||
"type":"archive",
|
||||
"url":"https://pypi.python.org/packages/8d/80/eca7a2d1a3c2dafb960f32f844d570de988e609f5fd17de92e1cf6a01b0a/Pillow-4.0.0.tar.gz#md5=d5af224b0fa2c66dacc9814785fef9e7",
|
||||
"sha256":"ee26d2d7e7e300f76ba7b796014c04011394d0c4a5ed9a288264a3e443abca50"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"gnome-screenshot",
|
||||
"sources":[
|
||||
{
|
||||
"type":"git",
|
||||
"url":"https://git.gnome.org/browse/gnome-screenshot",
|
||||
"branch":"gnome-3-22"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"gnome-keyring",
|
||||
"sources":[
|
||||
{
|
||||
"type":"git",
|
||||
"url":"https://git.gnome.org/browse/gnome-keyring/",
|
||||
"branch":"gnome-3-22"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"Gnome Authenticator",
|
||||
"buildsystem":"meson",
|
||||
"builddir":true,
|
||||
"sources":[
|
||||
{
|
||||
"type":"git",
|
||||
"url":"https://github.com/bil-elmoussaoui/Gnome-Authenticator",
|
||||
"branch":"master"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
81
gnome-authenticator.in
Normal file
81
gnome-authenticator.in
Normal file
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
|
||||
|
||||
This file is part of Gnome-TwoFactorAuth.
|
||||
|
||||
TwoFactorAuth is free software: you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gnome-TwoFactorAuth is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from gi import require_version
|
||||
require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
import sys
|
||||
sys.path.insert(1, '@PYTHONDIR@')
|
||||
from gi.repository import Gio
|
||||
from Authenticator import application
|
||||
import logging
|
||||
import locale
|
||||
import gettext
|
||||
_ = gettext.gettext
|
||||
import argparse
|
||||
import faulthandler
|
||||
from os import path, environ as env
|
||||
|
||||
if __name__ == "__main__":
|
||||
locale.bindtextdomain('Authenticator', '@LOCALE_DIR@')
|
||||
locale.textdomain('Authenticator')
|
||||
gettext.bindtextdomain('Authenticator', '@LOCALE_DIR@')
|
||||
gettext.textdomain('Authenticator')
|
||||
GIT_VERSION = "@GITVERSION@"
|
||||
VERSION = "@VERSION@"
|
||||
env["DATA_DIR"] = "@DATADIR@"
|
||||
env["LOCALE_DIR"] = '@LOCALE_DIR@'
|
||||
|
||||
parser = argparse.ArgumentParser(prog="Gnome Authenticator")
|
||||
parser.add_argument("--debug", "-d", action="store_true",
|
||||
help=_("Start in debug mode"))
|
||||
parser.add_argument("--version", "-v", action="store_true",
|
||||
help=_("Gnome Authenticator version number"))
|
||||
parser.add_argument("--about", action="store_true",
|
||||
help=_("Show about dialog"))
|
||||
args = parser.parse_args()
|
||||
|
||||
level = logging.ERROR
|
||||
if args.debug:
|
||||
level = logging.DEBUG
|
||||
faulthandler.enable()
|
||||
logging.basicConfig(level=level,
|
||||
format='[%(levelname)s] - %(asctime)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
resource = Gio.resource_load(path.join('@DATADIR@',
|
||||
'org.gnome.Authenticator.gresource'))
|
||||
Gio.Resource._register(resource)
|
||||
if args.version:
|
||||
if GIT_VERSION:
|
||||
print("Git current commit : " + str(GIT_VERSION))
|
||||
sys.exit("Version : " + str(VERSION))
|
||||
elif args.about:
|
||||
about_dialog = application.Application.about_dialog()
|
||||
about_dialog.run()
|
||||
about_dialog.destroy()
|
||||
sys.exit()
|
||||
else:
|
||||
try:
|
||||
app = application.Application()
|
||||
exit_status = app.run(None)
|
||||
sys.exit(exit_status)
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
237
po/org.gnome.Authenticator.pot
Normal file
237
po/org.gnome.Authenticator.pot
Normal file
|
@ -0,0 +1,237 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-12-13 00:20+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: ../TwoFactorAuth/application.py:46
|
||||
msgid "TwoFactorAuth"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/application.py:82 ../TwoFactorAuth/widgets/headerbar.py:117
|
||||
#: ../TwoFactorAuth/widgets/settings.py:42
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/application.py:90
|
||||
msgid "Shortcuts"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/application.py:92
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/application.py:93
|
||||
msgid "Quit"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:157
|
||||
#, python-format
|
||||
msgid "Code \"%s\" copied to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:222
|
||||
msgid "Remove the account"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:232
|
||||
msgid "Copy the generated code"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:242
|
||||
msgid "Edit the account"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:252
|
||||
msgid "Save the new account name"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:260
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:386
|
||||
#, python-format
|
||||
msgid "Expires in %s seconds"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:266
|
||||
msgid "Error during the generation of code"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:311
|
||||
msgid "Couldn't generate the secret code"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:373
|
||||
#, python-format
|
||||
msgid "Do you really want to remove \"%s\"?"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/account_row.py:377
|
||||
#, python-format
|
||||
msgid "\"%s\" was removed"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:50
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:67
|
||||
#: ../TwoFactorAuth/widgets/headerbar.py:71
|
||||
msgid "Add a new account"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:72
|
||||
#: ../TwoFactorAuth/widgets/applications_list.py:115
|
||||
#: ../TwoFactorAuth/widgets/change_password.py:162
|
||||
#: ../TwoFactorAuth/widgets/headerbar.py:105
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:77
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:85
|
||||
msgid "Scan a QR code"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:107
|
||||
msgid "Account Name"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:115
|
||||
msgid "Secret Code"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/add_account.py:145
|
||||
msgid "Selected area is not a valid QR code"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/applications_list.py:110
|
||||
msgid "Select an application"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/applications_list.py:122
|
||||
#: ../TwoFactorAuth/widgets/headerbar.py:101
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/applications_list.py:125
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/change_password.py:36
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/change_password.py:46
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/change_password.py:78
|
||||
msgid "Old password"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/change_password.py:88
|
||||
msgid "New password"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/change_password.py:98
|
||||
msgid "Repeat new password"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/headerbar.py:64
|
||||
msgid "Remove selected accounts"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/headerbar.py:78
|
||||
msgid "Lock the Application"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/headerbar.py:94
|
||||
msgid "Selection mode"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/inapp_notification.py:28
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/inapp_notification.py:39
|
||||
msgid "Hide notification"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/login_window.py:45
|
||||
msgid "Enter your password"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/login_window.py:48
|
||||
msgid "Unlock"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/no_account_window.py:39
|
||||
msgid "There's no account at the moment"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/settings.py:70
|
||||
msgid "Behavior"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/settings.py:75
|
||||
msgid "Account"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/settings.py:89
|
||||
msgid "Password protection"
|
||||
msgstr ""
|
||||
|
||||
#: ../TwoFactorAuth/widgets/settings.py:113
|
||||
msgid "Auto-lock the application (m):"
|
||||
msgstr ""
|
||||
|
||||
#: ../data/gnome-twofactorauth.appdata.xml.in.h:1
|
||||
#: ../data/gnome-twofactorauth.desktop.in.h:1
|
||||
msgid "Gnome TwoFactorAuth"
|
||||
msgstr ""
|
||||
|
||||
#: ../data/gnome-twofactorauth.appdata.xml.in.h:2
|
||||
msgid "Two Factor Authentication code generator"
|
||||
msgstr ""
|
||||
|
||||
#: ../data/gnome-twofactorauth.appdata.xml.in.h:3
|
||||
msgid ""
|
||||
"Simple application that generates two-factor authentication code, created "
|
||||
"for Gnome"
|
||||
msgstr ""
|
||||
|
||||
#: ../data/gnome-twofactorauth.desktop.in.h:2
|
||||
msgid "Two Factor Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: ../data/gnome-twofactorauth.desktop.in.h:3
|
||||
msgid "Two-Factor Authentication code generator"
|
||||
msgstr ""
|
||||
|
||||
#: ../data/gnome-twofactorauth.desktop.in.h:4
|
||||
msgid "About Gnome TwoFactorAuth"
|
||||
msgstr ""
|
||||
|
||||
#: ../gnome-twofactorauth.in:50
|
||||
msgid "Start in debug mode"
|
||||
msgstr ""
|
||||
|
||||
#: ../gnome-twofactorauth.in:52
|
||||
msgid "Gnome-TwoFactorAuth version number"
|
||||
msgstr ""
|
||||
|
||||
#: ../gnome-twofactorauth.in:54
|
||||
msgid "Show about dialog"
|
||||
msgstr ""
|
Loading…
Add table
Reference in a new issue