store passwords in database
This commit is contained in:
parent
3ff8e4886f
commit
b6a085b538
|
@ -3,7 +3,6 @@ from ..database import default_permissions
|
||||||
from ..exceptions import AccountNotFoundError, NoAccountsError
|
from ..exceptions import AccountNotFoundError, NoAccountsError
|
||||||
from ..functions import SignalBlock, connect, get_buffer_text
|
from ..functions import SignalBlock, connect, get_buffer_text
|
||||||
from ..objects.login_rows import SavedLoginRow
|
from ..objects.login_rows import SavedLoginRow
|
||||||
from ..passwords import passdb
|
|
||||||
|
|
||||||
|
|
||||||
class StatusBar:
|
class StatusBar:
|
||||||
|
@ -392,8 +391,9 @@ class StatusBar:
|
||||||
for child in login_list.get_children():
|
for child in login_list.get_children():
|
||||||
child.destroy()
|
child.destroy()
|
||||||
|
|
||||||
for row in passdb.fetch(domain=self.window.active_tab.url.hostname()):
|
with self.db.session as s:
|
||||||
login_list.add(SavedLoginRow(row, self.window.active_tab.url)['container'])
|
for row in s.fetch('passwords', domain=self.window.active_tab.url.hostname()):
|
||||||
|
login_list.add(SavedLoginRow(row, self.window.active_tab.url)['container'])
|
||||||
|
|
||||||
|
|
||||||
def handle_toot_key_press(self, textview, event, *args):
|
def handle_toot_key_press(self, textview, event, *args):
|
||||||
|
|
|
@ -7,7 +7,6 @@ from .web_view import Webview
|
||||||
|
|
||||||
from .. import var, __software__
|
from .. import var, __software__
|
||||||
from ..functions import Thread, run_in_gui_thread, connect, get_app, icon_set
|
from ..functions import Thread, run_in_gui_thread, connect, get_app, icon_set
|
||||||
from ..passwords import passdb
|
|
||||||
from ..themes import Themes
|
from ..themes import Themes
|
||||||
from ..widgets import FileChooser, Menu, MenuButtonRefresh
|
from ..widgets import FileChooser, Menu, MenuButtonRefresh
|
||||||
|
|
||||||
|
@ -548,7 +547,6 @@ class Window(Gtk.ApplicationWindow):
|
||||||
def handle_window_close(self, *args):
|
def handle_window_close(self, *args):
|
||||||
logging.verbose('Saving data')
|
logging.verbose('Saving data')
|
||||||
|
|
||||||
passdb.disconnect()
|
|
||||||
self.save_tabs()
|
self.save_tabs()
|
||||||
|
|
||||||
with self.app.db.session as s:
|
with self.app.db.session as s:
|
||||||
|
|
|
@ -78,6 +78,17 @@ default_searches = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
password_fields = [
|
||||||
|
'username',
|
||||||
|
'domain',
|
||||||
|
'label',
|
||||||
|
'password',
|
||||||
|
'label',
|
||||||
|
'url',
|
||||||
|
'note'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
tables = {
|
tables = {
|
||||||
'config': [
|
'config': [
|
||||||
Column('id'),
|
Column('id'),
|
||||||
|
@ -160,23 +171,16 @@ tables = {
|
||||||
Column('count', 'integer', nullable=False, default=0),
|
Column('count', 'integer', nullable=False, default=0),
|
||||||
Column('last_access', 'datetime')
|
Column('last_access', 'datetime')
|
||||||
],
|
],
|
||||||
'passfields': [
|
|
||||||
Column('id'),
|
|
||||||
Column('domain', 'text', nullable=False),
|
|
||||||
Column('url', 'text', nullable=False),
|
|
||||||
Column('userfield', 'text', nullable=False),
|
|
||||||
Column('passfield', 'text', nullable=False)
|
|
||||||
],
|
|
||||||
'passwords': [
|
'passwords': [
|
||||||
Column('id'),
|
Column('id'),
|
||||||
Column('username', 'text', nullable=False),
|
Column('username', 'text', nullable=False),
|
||||||
Column('password', 'text', nullable=False),
|
Column('password', 'text', nullable=False),
|
||||||
Column('name', 'text', nullable=False),
|
Column('label', 'text', nullable=False),
|
||||||
Column('domain', 'text'),
|
Column('domain', 'text', nullable=False),
|
||||||
Column('url', 'text'),
|
Column('url', 'text'),
|
||||||
Column('note', 'text', nullable=False),
|
Column('note', 'text'),
|
||||||
Column('created', 'datetime', nullable=False),
|
Column('created', 'datetime', nullable=False),
|
||||||
Column('modified', 'datetime', nullable=False)
|
Column('modified', 'datetime')
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,8 +294,8 @@ class CustomSession(Session):
|
||||||
return self.get_cached('history', url, url=url)
|
return self.get_cached('history', url, url=url)
|
||||||
|
|
||||||
|
|
||||||
def get_passfield(self, url):
|
def get_password(self, username, host):
|
||||||
return self.get_cached('passfields', url.without_query, url=url.without_query)
|
return self.fetch('passwords', username=username, domain=host).one()
|
||||||
|
|
||||||
|
|
||||||
def get_permission(self, domain, cache=True):
|
def get_permission(self, domain, cache=True):
|
||||||
|
@ -453,6 +457,41 @@ class CustomSession(Session):
|
||||||
return self.put_history(tab.url, tab.title, (True if tab.fedi_post else False))
|
return self.put_history(tab.url, tab.title, (True if tab.fedi_post else False))
|
||||||
|
|
||||||
|
|
||||||
|
def put_password(self, username, domain, password, url=None, label=None, note=None):
|
||||||
|
data = DotDict(
|
||||||
|
username = username,
|
||||||
|
domain = domain,
|
||||||
|
password = password,
|
||||||
|
created = datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
for key, value in (('url', url), ('label', label), ('note', note)):
|
||||||
|
if value:
|
||||||
|
data[key] = value
|
||||||
|
|
||||||
|
if (row := self.get_password(username, domain)):
|
||||||
|
return self.put_password_row(row, **data)
|
||||||
|
|
||||||
|
if not data.get('label'):
|
||||||
|
data['label'] = f'{username} @ {domain}'
|
||||||
|
|
||||||
|
self.insert('passwords', **data)
|
||||||
|
|
||||||
|
return self.get_password(username, domain)
|
||||||
|
|
||||||
|
|
||||||
|
def put_password_row(self, row, **kwargs):
|
||||||
|
for key in kwargs.keys():
|
||||||
|
if key not in password_fields:
|
||||||
|
raise KeyError(f'Invalid password field: {key}')
|
||||||
|
|
||||||
|
kwargs.update({'modified': datetime.now()})
|
||||||
|
self.update_row(row, **kwargs)
|
||||||
|
row.update(**kwargs)
|
||||||
|
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
def put_passfield(self, url, userfield, passfield):
|
def put_passfield(self, url, userfield, passfield):
|
||||||
if None in [userfield, passfield]:
|
if None in [userfield, passfield]:
|
||||||
logging.verbose('Not inserting empty login fields')
|
logging.verbose('Not inserting empty login fields')
|
||||||
|
|
|
@ -4,7 +4,6 @@ from .base import tables
|
||||||
|
|
||||||
from .. import var
|
from .. import var
|
||||||
from ..functions import get_app
|
from ..functions import get_app
|
||||||
from ..passwords import passdb
|
|
||||||
|
|
||||||
|
|
||||||
def now(db, version):
|
def now(db, version):
|
||||||
|
@ -33,11 +32,9 @@ def now(db, version):
|
||||||
for keyword, data in s.default_searches.items():
|
for keyword, data in s.default_searches.items():
|
||||||
s.put_search(data[0], keyword, data[1])
|
s.put_search(data[0], keyword, data[1])
|
||||||
|
|
||||||
if version <= 20210905:
|
if version < 20210905:
|
||||||
s.create_tables({'passwords': tables['passwords']})
|
s.create_tables({'passwords': tables['passwords']})
|
||||||
|
s.drop_table('passfields')
|
||||||
else:
|
|
||||||
raise KeyError('heck')
|
|
||||||
|
|
||||||
s.put_config('version', var.dbversion, 'int')
|
s.put_config('version', var.dbversion, 'int')
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from mastodon import Mastodon
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from ..functions import get_app
|
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
|
||||||
|
|
||||||
|
|
||||||
row_classes = {}
|
row_classes = {}
|
||||||
|
@ -22,8 +22,14 @@ def register_class(table):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class RowBase(Row):
|
||||||
|
@property
|
||||||
|
def app(self):
|
||||||
|
return get_app()
|
||||||
|
|
||||||
|
|
||||||
@register_class('accounts')
|
@register_class('accounts')
|
||||||
class Account(Row):
|
class Account(RowBase):
|
||||||
_api = None
|
_api = None
|
||||||
_emojis = None
|
_emojis = None
|
||||||
|
|
||||||
|
@ -178,8 +184,21 @@ class Account(Row):
|
||||||
s.put_config('active_acct', self.id)
|
s.put_config('active_acct', self.id)
|
||||||
|
|
||||||
|
|
||||||
|
@register_class('passwords')
|
||||||
|
class Password(RowBase):
|
||||||
|
def copy_password(self, timeout=60):
|
||||||
|
self.app.set_clipboard_text(self.password)
|
||||||
|
|
||||||
|
timer = TimeoutCallback(timeout, run_in_gui_thread, self.app.handle_clipboard_clear_password, self.password)
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
|
||||||
|
def copy_username(self):
|
||||||
|
self.app.set_clipboard_text(self.username)
|
||||||
|
|
||||||
|
|
||||||
@register_class('search')
|
@register_class('search')
|
||||||
class Search(Row):
|
class Search(RowBase):
|
||||||
def compile(self, text):
|
def compile(self, text):
|
||||||
if text.startswith(f'{self.keyword} '):
|
if text.startswith(f'{self.keyword} '):
|
||||||
text = text[len(self.keyword)+1:]
|
text = text[len(self.keyword)+1:]
|
||||||
|
|
|
@ -37,6 +37,15 @@ class BarksharkWebExtension:
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
try:
|
||||||
|
if object.__getattribute__(self, 'manifest'):
|
||||||
|
return self.manifest[key]
|
||||||
|
|
||||||
|
finally:
|
||||||
|
return object.__getattribute(self, key)
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.config[key]
|
return self.config[key]
|
||||||
|
|
||||||
|
@ -91,23 +100,23 @@ class BarksharkWebExtension:
|
||||||
self.logging.setConfig({'level': level})
|
self.logging.setConfig({'level': level})
|
||||||
|
|
||||||
|
|
||||||
def handle_initialize(self, client, tab):
|
def handle_initialize(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def handle_page_created(self, client, tab):
|
def handle_page_created(self, pageid: int):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def handle_document_loaded(self, client, tab, page_url: str):
|
def handle_document_loaded(self, page_url: Url):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def handle_send_request(self, client, tab, page_url: str, request: dict, redirect: dict):
|
def handle_send_request(self, page_url: Url, redirect: Url):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def handle_console_message(self, client, tab, page_url: str, text: str, level: str, line: int, source: str, source_id: int):
|
def handle_console_message_sent(self, text: str, level: str, line: int):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -115,5 +124,5 @@ class BarksharkWebExtension:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def handle_form_sent(self, data, stage):
|
def handle_form_sent(self, stage: str, fields: DotDict):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -86,40 +86,50 @@ class WebExtensions(ObjectBase):
|
||||||
assert message.get_name() not in ['command']
|
assert message.get_name() not in ['command']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
func = getattr(self, f'handle_{cmd_name}')
|
func = getattr(self, f'parse_{cmd_name}')
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return logging.error(f'Invalid command: {cmd_name}')
|
return logging.error(f'Invalid command: {cmd_name}')
|
||||||
|
|
||||||
func(*message.get_parameters())
|
try:
|
||||||
|
args, kwargs = func(*message.get_parameters())
|
||||||
|
|
||||||
|
except:
|
||||||
def handle_page_created(self, page_id):
|
traceback.print_exc()
|
||||||
return
|
logging.error('Error parsing data for signal:', cmd_name)
|
||||||
print(f'page-created: {page_id}')
|
return
|
||||||
|
|
||||||
|
|
||||||
def handle_form_sent(self, stage, raw_data):
|
|
||||||
raw_data = DotDict(raw_data)
|
|
||||||
|
|
||||||
for ext in (*self.system.values(), *self.user.values()):
|
for ext in (*self.system.values(), *self.user.values()):
|
||||||
try:
|
try:
|
||||||
ext.handle_form_sent(
|
getattr(ext, f'handle_{cmd_name}')(*args, **kwargs)
|
||||||
DotDict(dict(zip(raw_data['keys'], raw_data['values']))),
|
|
||||||
stage.replace('WEBKIT_FORM_SUBMISSION_WILL_', '')
|
|
||||||
)
|
|
||||||
|
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
logging.error('Error handling handle_form_sent for extension:', ext.name)
|
logging.error('Error handling handle_form_sent for extension:', ext.name)
|
||||||
|
|
||||||
|
|
||||||
def handle_console_message_sent(self, text, level, line):
|
def parse_page_created(self, page_id):
|
||||||
|
return (page_id, ), {}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_form_sent(self, stage, raw_data):
|
||||||
|
raw_data = DotDict(raw_data)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
DotDict(dict(zip(raw_data['keys'], raw_data['values']))),
|
||||||
|
stage.replace('WEBKIT_FORM_SUBMISSION_WILL_', '')
|
||||||
|
]
|
||||||
|
|
||||||
|
return args, {}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_console_message_sent(self, text, level, line):
|
||||||
log_level = level.replace('WEBKIT_CONSOLE_MESSAGE_LEVEL_', '').upper()
|
log_level = level.replace('WEBKIT_CONSOLE_MESSAGE_LEVEL_', '').upper()
|
||||||
|
|
||||||
if log_level == 'LOG':
|
if log_level == 'LOG':
|
||||||
log_level = 'INFO'
|
log_level = 'INFO'
|
||||||
|
|
||||||
#logging.log(LogLevel[log_level], text.strip())
|
return (text, level, line), {}
|
||||||
|
|
||||||
|
|
||||||
class ExtPasswordManager(BarksharkWebExtension):
|
class ExtPasswordManager(BarksharkWebExtension):
|
||||||
|
|
|
@ -175,3 +175,14 @@ function copy_password(id) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setup_password_handlers() {
|
||||||
|
connect_event("password", "focus", (event) => {
|
||||||
|
event.currentTarget.type = "text";
|
||||||
|
});
|
||||||
|
|
||||||
|
connect_event("password", "blur", (event) => {
|
||||||
|
event.currentTarget.type = "password";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
-if password
|
-if password
|
||||||
-set page = 'Edit Password: ' + password.label
|
-set page = 'Edit Password: ' + password.label
|
||||||
-set action = var.local + '/passwords/update/' + password.id
|
-set action = var.local + '/passwords/update/' + str(password.id)
|
||||||
-else
|
-else
|
||||||
-set page = 'New Password'
|
-set page = 'New Password'
|
||||||
-set action = var.local + '/passwords/update'
|
-set action = var.local + '/passwords/update'
|
||||||
|
@ -15,18 +15,18 @@
|
||||||
%input type='hidden' name='redir' value='/passwords'
|
%input type='hidden' name='redir' value='/passwords'
|
||||||
|
|
||||||
.grid-container
|
.grid-container
|
||||||
|
%label.grid-item for='label' << Label
|
||||||
|
%input.grid-item id='label' class='full-width' type='text' name='label' placeholder='label' value='{{password.label or ""}}'
|
||||||
|
|
||||||
%label.grid-item for='username' << Username
|
%label.grid-item for='username' << Username
|
||||||
%input.grid-item id='username' class='full-width' type='text' name='username' placeholder='username' value='{{password.username}}'
|
%input.grid-item id='username' class='full-width' type='text' name='username' placeholder='username' value='{{password.username}}'
|
||||||
|
|
||||||
%label.grid-item for='password' << Password
|
%label.grid-item for='password' << Password
|
||||||
%input.grid-item id='password' class='full-width' type='password' name='password' placeholder='password' value='{{password.password}}'
|
%input#password.grid-item id='password' class='full-width' type='password' name='password' placeholder='password' value='{{password.password}}'
|
||||||
|
|
||||||
%label.grid-item for='domain' << Domain
|
%label.grid-item for='domain' << Domain
|
||||||
%input.grid-item id='domain' class='full-width' type='text' name='domain' placeholder='domain' value='{{password.domain}}'
|
%input.grid-item id='domain' class='full-width' type='text' name='domain' placeholder='domain' value='{{password.domain}}'
|
||||||
|
|
||||||
%label.grid-item for='label' << Label
|
|
||||||
%input.grid-item id='label' class='full-width' type='text' name='label' placeholder='label' value='{{password.label}}'
|
|
||||||
|
|
||||||
%label.grid-item for='url' << Url
|
%label.grid-item for='url' << Url
|
||||||
%input.grid-item id='url' class='full-width' type='text' name='url' placeholder='url' value='{{password.url or ""}}'
|
%input.grid-item id='url' class='full-width' type='text' name='url' placeholder='url' value='{{password.url or ""}}'
|
||||||
|
|
||||||
|
@ -40,3 +40,6 @@
|
||||||
%a.button onclick='wine.location="{{var.local}}/passwords/delete/{{password.id}}?redir=/passwords"' << Delete
|
%a.button onclick='wine.location="{{var.local}}/passwords/delete/{{password.id}}?redir=/passwords"' << Delete
|
||||||
|
|
||||||
%a.button onclick='window.location.href="{{var.local}}/passwords"' << Cancel
|
%a.button onclick='window.location.href="{{var.local}}/passwords"' << Cancel
|
||||||
|
|
||||||
|
%script type='application/javascript'
|
||||||
|
setup_password_handlers()
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
-if items
|
-if items
|
||||||
-for row in items
|
-for row in items
|
||||||
%details.item id='{{row.id}}' open
|
%details.item id='{{row.id}}'
|
||||||
%summary << {{row.label}}
|
%summary << {{row.label}}
|
||||||
|
|
||||||
.details-indent
|
.details-indent
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from ..functions import TimeoutCallback, connect, get_app, run_in_gui_thread
|
from ..functions import TimeoutCallback, connect, get_app, run_in_gui_thread
|
||||||
from ..passwords import passdb
|
|
||||||
|
|
||||||
|
|
||||||
class LoginRowBase:
|
class LoginRowBase:
|
||||||
|
@ -14,7 +13,7 @@ class LoginRowBase:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self):
|
def db(self):
|
||||||
return self.ap.db
|
return self.app.db
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -35,14 +34,8 @@ class SavedLoginRow(LoginRowBase):
|
||||||
self.ui = Gtk.Builder.new_from_file(self.app.path.resources.join('password_saved.ui'))
|
self.ui = Gtk.Builder.new_from_file(self.app.path.resources.join('password_saved.ui'))
|
||||||
self.row = row
|
self.row = row
|
||||||
|
|
||||||
with self.db.session as s:
|
|
||||||
self.dbrow = s.get_passfield(page_url)
|
|
||||||
|
|
||||||
self['username'].set_text(self.row['username'])
|
self['username'].set_text(self.row['username'])
|
||||||
|
|
||||||
#if self.row.url != page_url or not self.dbrow:
|
|
||||||
#self['fill'].set_sensitive(False)
|
|
||||||
|
|
||||||
self.connect('fill', 'clicked', self.handle_fill_password)
|
self.connect('fill', 'clicked', self.handle_fill_password)
|
||||||
self.connect('copy-password', 'clicked', self.handle_copy_password)
|
self.connect('copy-password', 'clicked', self.handle_copy_password)
|
||||||
self.connect('copy-username', 'clicked', self.row.copy_username)
|
self.connect('copy-username', 'clicked', self.row.copy_username)
|
||||||
|
@ -75,9 +68,6 @@ class UnsavedLoginRow(LoginRowBase):
|
||||||
self.ext = ext
|
self.ext = ext
|
||||||
self.form = form
|
self.form = form
|
||||||
|
|
||||||
with self.db.session as s:
|
|
||||||
self.row = s.get_passfield(form.form_url)
|
|
||||||
|
|
||||||
self.set_data(form.username[1], form.password[1])
|
self.set_data(form.username[1], form.password[1])
|
||||||
|
|
||||||
self.connect('save', 'clicked', self.handle_save)
|
self.connect('save', 'clicked', self.handle_save)
|
||||||
|
@ -123,6 +113,7 @@ class UnsavedLoginRow(LoginRowBase):
|
||||||
def handle_save(self):
|
def handle_save(self):
|
||||||
data = self.get_data()
|
data = self.get_data()
|
||||||
|
|
||||||
passdb.insert(data.username, data.url.domain, data.password, data.url, data.note)
|
with get_app().db.session as s:
|
||||||
self.window.passwords.refresh()
|
s.put_password(data.username, data.password, data.url, name=None, note=data.note)
|
||||||
|
|
||||||
self.handle_cancel()
|
self.handle_cancel()
|
||||||
|
|
|
@ -1,335 +0,0 @@
|
||||||
import json, secretstorage
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from secretstorage.collection import Collection, get_collection_by_alias, create_collection
|
|
||||||
from secretstorage.exceptions import ItemNotFoundException, LockedException
|
|
||||||
|
|
||||||
from .functions import TimeoutCallback, get_app, run_in_gui_thread
|
|
||||||
|
|
||||||
|
|
||||||
pass_store_keys = ['username', 'domain', 'url', 'note']
|
|
||||||
pass_keys = [*pass_store_keys, 'created', 'modified', 'label', 'password']
|
|
||||||
|
|
||||||
|
|
||||||
def parse_data(username=None, domain=None, password=None, url=None, note=None, label=None, id=None, **kwargs):
|
|
||||||
new_data = DotDict(
|
|
||||||
id = id,
|
|
||||||
username = username,
|
|
||||||
domain = domain,
|
|
||||||
url = url,
|
|
||||||
note = note,
|
|
||||||
password = password,
|
|
||||||
label = label
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, value in tuple(new_data.items()):
|
|
||||||
if not value:
|
|
||||||
del new_data[key]
|
|
||||||
|
|
||||||
return new_data
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordStorage:
|
|
||||||
def __init__(self, name='BarksharkWeb'):
|
|
||||||
self.name = name
|
|
||||||
self.connection = None
|
|
||||||
self.collection = None
|
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
if (item := self.fetch(id=key).one()):
|
|
||||||
return item
|
|
||||||
|
|
||||||
raise KeyError(f'No password with label: {key}')
|
|
||||||
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if (item := self[key]):
|
|
||||||
item.update(**value)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.insert(label=key, **value)
|
|
||||||
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.connect()
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
def __exit__(self, *args):
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
if self.connection and self.collection:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.connection:
|
|
||||||
self.connection = secretstorage.dbus_init()
|
|
||||||
|
|
||||||
if not secretstorage.check_service_availability(self.connection):
|
|
||||||
self.disconnect()
|
|
||||||
raise ConnectionError('Failed to connect to Secret Service server')
|
|
||||||
|
|
||||||
return self.get_keyring()
|
|
||||||
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
if self.connection:
|
|
||||||
self.connection.close()
|
|
||||||
|
|
||||||
self.connection = None
|
|
||||||
self.collection = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_keyring(self):
|
|
||||||
if not self.collection:
|
|
||||||
try:
|
|
||||||
self.collection = Collection(self.connection, f'/org/freedesktop/secrets/collection/{self.name}')
|
|
||||||
except ItemNotFoundException:
|
|
||||||
create_collection(self.connection, self.name)
|
|
||||||
self.collection = Collection(self.connection, f'/org/freedesktop/secrets/collection/{self.name}')
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.collection.ensure_not_locked()
|
|
||||||
except LockedException:
|
|
||||||
self.unlock()
|
|
||||||
|
|
||||||
return self.collection
|
|
||||||
|
|
||||||
|
|
||||||
def unlock(self):
|
|
||||||
return self.collection.unlock()
|
|
||||||
|
|
||||||
|
|
||||||
def lock(self):
|
|
||||||
return self.collection.lock()
|
|
||||||
|
|
||||||
|
|
||||||
def fetch(self, *args, **kwargs):
|
|
||||||
data = parse_data(*args, **kwargs)
|
|
||||||
data.pop('password', None)
|
|
||||||
|
|
||||||
if not any(data.values()):
|
|
||||||
rows = self.collection.get_all_items()
|
|
||||||
|
|
||||||
else:
|
|
||||||
rows = self.collection.search_items(data)
|
|
||||||
|
|
||||||
return PasswordResult(rows)
|
|
||||||
|
|
||||||
|
|
||||||
def insert(self, *args, label=None, **kwargs):
|
|
||||||
data = parse_data(*args, **kwargs)
|
|
||||||
data['id'] = random_str()
|
|
||||||
required = {key: data.get(key) for key in ['username', 'domain', 'password']}
|
|
||||||
|
|
||||||
if None in required.values():
|
|
||||||
raise ValueError(f'Forgot username, domain, or password: {json.dumps(required)}')
|
|
||||||
|
|
||||||
username = data['username']
|
|
||||||
password = data['password']
|
|
||||||
password = data.pop('password')
|
|
||||||
|
|
||||||
if (row := self.fetch(username, password).one()):
|
|
||||||
logging.verbose('Password already exists:', row.label)
|
|
||||||
return row
|
|
||||||
|
|
||||||
if not label:
|
|
||||||
label = f'{username} @ {password}'
|
|
||||||
|
|
||||||
row = self.collection.create_item(label, data, password.encode())
|
|
||||||
return PasswordItem(row)
|
|
||||||
|
|
||||||
|
|
||||||
def update(self, username, domain, **kwargs):
|
|
||||||
row = self.fetch(username, domain).one()
|
|
||||||
row.update(**kwargs)
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def remove(self, *args, **kwargs):
|
|
||||||
for row in self.fetch(*args, **kwargs):
|
|
||||||
row.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResult:
|
|
||||||
def __init__(self, items):
|
|
||||||
self._items = items
|
|
||||||
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
for item in self.all():
|
|
||||||
yield item
|
|
||||||
|
|
||||||
|
|
||||||
def all(self):
|
|
||||||
for item in self._items:
|
|
||||||
yield PasswordItem(item)
|
|
||||||
|
|
||||||
|
|
||||||
def one(self):
|
|
||||||
for item in self.all():
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordItem:
|
|
||||||
def __init__(self, item):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self._item = item
|
|
||||||
self._attrs = item.get_attributes()
|
|
||||||
|
|
||||||
if not self._attrs.get('id'):
|
|
||||||
self.update({'id': random_str()})
|
|
||||||
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'PasswordItem(id={self.id}, username={self.username}, domain={self.domain})'
|
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
if key not in pass_keys:
|
|
||||||
raise KeyError(key)
|
|
||||||
|
|
||||||
return getattr(self, key)
|
|
||||||
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if key not in pass_store_keys:
|
|
||||||
raise KeyError(key)
|
|
||||||
|
|
||||||
self.update(**{key: value})
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def created(self):
|
|
||||||
return datetime.fromtimestamp(self._item.get_created())
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def modified(self):
|
|
||||||
return datetime.fromtimestamp(self._item.get_modified())
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def label(self):
|
|
||||||
if not (label := self._item.get_label()):
|
|
||||||
return f'{self.username} @ {self.domain}'
|
|
||||||
|
|
||||||
return label
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self):
|
|
||||||
return self._attrs.get('id')
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def password(self):
|
|
||||||
return self._item.get_secret().decode()
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def username(self):
|
|
||||||
return self._attrs.get('username')
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def domain(self):
|
|
||||||
return self._attrs.get('domain')
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self):
|
|
||||||
if (url := self._attrs.get('url')):
|
|
||||||
return Url(url)
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def note(self):
|
|
||||||
return self._attrs.get('note')
|
|
||||||
|
|
||||||
|
|
||||||
@password.setter
|
|
||||||
def password(self, value):
|
|
||||||
self.update(password=value)
|
|
||||||
|
|
||||||
|
|
||||||
@username.setter
|
|
||||||
def username(self, value):
|
|
||||||
self.update(username=value)
|
|
||||||
|
|
||||||
|
|
||||||
@domain.setter
|
|
||||||
def domain(self, value):
|
|
||||||
self.update(domain=value)
|
|
||||||
|
|
||||||
|
|
||||||
@url.setter
|
|
||||||
def url(self, value):
|
|
||||||
return self.update(url=str(value))
|
|
||||||
|
|
||||||
|
|
||||||
@note.setter
|
|
||||||
def note(self, value):
|
|
||||||
return self.update(note=value)
|
|
||||||
|
|
||||||
|
|
||||||
def copy_password(self, timeout=60):
|
|
||||||
app = get_app()
|
|
||||||
app.set_clipboard_text(self.password)
|
|
||||||
|
|
||||||
timer = TimeoutCallback(timeout, run_in_gui_thread, app.handle_clipboard_clear_password, self.password)
|
|
||||||
timer.start()
|
|
||||||
|
|
||||||
|
|
||||||
def copy_username(self):
|
|
||||||
get_app().set_clipboard_text(self.username)
|
|
||||||
|
|
||||||
|
|
||||||
def as_dict(self):
|
|
||||||
return DotDict(
|
|
||||||
id = self.id,
|
|
||||||
domain = self.domain,
|
|
||||||
username = self.username,
|
|
||||||
password = self.password,
|
|
||||||
url = self.url,
|
|
||||||
note = self.note,
|
|
||||||
created = self.created,
|
|
||||||
modified = self.modified
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def to_json(self, indent=None):
|
|
||||||
return self.as_dict().to_json(indent)
|
|
||||||
|
|
||||||
|
|
||||||
def update(self, data):
|
|
||||||
label = data.pop('label', None)
|
|
||||||
|
|
||||||
if (password := data.pop('password', None)):
|
|
||||||
self._item.set_secret(password.encode())
|
|
||||||
|
|
||||||
if not len(data):
|
|
||||||
return
|
|
||||||
|
|
||||||
new_data = parse_data(**self.as_dict())
|
|
||||||
new_data.update(parse_data(**data))
|
|
||||||
|
|
||||||
self._item.set_attributes(new_data)
|
|
||||||
self._attrs = self._item.get_attributes()
|
|
||||||
|
|
||||||
if label:
|
|
||||||
self._item.set_label(label)
|
|
||||||
|
|
||||||
elif any(map(data.get, ['username', 'domain'])) and not self._item.get_label():
|
|
||||||
self._item.set_label(self.label)
|
|
||||||
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
self._item.delete()
|
|
||||||
|
|
||||||
|
|
||||||
passdb = PasswordStorage()
|
|
||||||
passdb.connect()
|
|
|
@ -9,7 +9,6 @@ from .functions import error, finish_request, finish_request_error, redirect
|
||||||
|
|
||||||
from .. import var
|
from .. import var
|
||||||
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
|
from ..functions import TimeoutCallback, get_app, run_in_gui_thread
|
||||||
from ..passwords import passdb
|
|
||||||
|
|
||||||
|
|
||||||
class LocalRequest(ProtocolRequest):
|
class LocalRequest(ProtocolRequest):
|
||||||
|
@ -292,19 +291,19 @@ def history_clear(handler, request):
|
||||||
def passwords_home(handler, request):
|
def passwords_home(handler, request):
|
||||||
domain = request.query.get('domain')
|
domain = request.query.get('domain')
|
||||||
|
|
||||||
if (search_text := request.query.get('text')):
|
with handler.db.session as s:
|
||||||
items = []
|
if (search_text := request.query.get('text')):
|
||||||
rows = passdb.fetch(domain=domain)
|
items = []
|
||||||
|
|
||||||
for row in rows:
|
for row in s.fetch('passwords', domain=domain):
|
||||||
if True in fuzzy_string_match(search_text, row.domain, row.url, row.username, row.note, row.label):
|
if True in fuzzy_string_match(search_text, row.domain, row.url, row.username, row.note, row.label):
|
||||||
items.append(row)
|
items.append(row)
|
||||||
|
|
||||||
elif domain:
|
elif domain:
|
||||||
items = passdb.fetch(domain=domain).all()
|
items = s.fetch('passwords', domain=domain).all()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
items = passdb.fetch().all()
|
items = s.fetch('passwords').all()
|
||||||
|
|
||||||
return request.page('passwords', {'items': items})
|
return request.page('passwords', {'items': items})
|
||||||
|
|
||||||
|
@ -314,60 +313,61 @@ def passwords_edit(handler, request, rowid=None):
|
||||||
if not rowid:
|
if not rowid:
|
||||||
return request.page('password-edit', {'password': None})
|
return request.page('password-edit', {'password': None})
|
||||||
|
|
||||||
try:
|
with handler.db.session as s:
|
||||||
item = passdb[rowid]
|
if not (item := s.fetch('passwords', id=rowid).one()):
|
||||||
|
return request.error(f'Cannot find password: {rowid}', 404)
|
||||||
except KeyError:
|
|
||||||
return request.error(f'Cannot find password: {rowid}', 404)
|
|
||||||
|
|
||||||
return request.page('password-edit', {'password': item})
|
return request.page('password-edit', {'password': item})
|
||||||
|
|
||||||
|
|
||||||
@Local.route('/passwords/update', '/passwords/update/{rowid}')
|
@Local.route('/passwords/update', '/passwords/update/{rowid}')
|
||||||
def passwords_update(handler, request, rowid=None):
|
def passwords_update(handler, request, rowid=None):
|
||||||
query = request.query
|
query = request.query.copy()
|
||||||
|
query.pop('redir', None)
|
||||||
|
|
||||||
|
print(query.get('label'))
|
||||||
|
|
||||||
if not query.get('label'):
|
if not query.get('label'):
|
||||||
query['label'] = f'{query.username} @ {query.domain}'
|
query['label'] = f'{query.username} @ {query.domain}'
|
||||||
|
|
||||||
if not rowid:
|
print(query.get('label'))
|
||||||
row = passdb.insert(**query)
|
|
||||||
|
|
||||||
return request.ok_or_redirect(f'Created new password: {row.label}')
|
for key, value in list(query.items()):
|
||||||
|
if isinstance(value, str) and not value:
|
||||||
|
del query[key]
|
||||||
|
|
||||||
try:
|
with handler.db.session as s:
|
||||||
row = passdb[rowid]
|
if not rowid:
|
||||||
row.update(query)
|
row = s.put_password(**query)
|
||||||
|
return request.ok_or_redirect(f'Created new password: {row.label}')
|
||||||
|
|
||||||
except KeyError:
|
if not (row := s.fetch('passwords', id=rowid).one()):
|
||||||
return request.error(f'Cannot find password: {rowid}', 404)
|
return request.error(f'Cannot find password: {rowid}', 404)
|
||||||
|
|
||||||
return request.ok_or_redirect(f'Updated password: {row.label}')
|
row = s.put_password_row(row, **query)
|
||||||
|
|
||||||
|
return request.ok_or_redirect(f'updated password: {row.label}')
|
||||||
|
|
||||||
|
|
||||||
@Local.route('/passwords/copy/{rowid}')
|
@Local.route('/passwords/copy/{rowid}')
|
||||||
def passwords_copy(handler, request, rowid):
|
def passwords_copy(handler, request, rowid):
|
||||||
try:
|
with handler.db.session as s:
|
||||||
row = passdb[rowid]
|
if not (row := s.fetch('passwords', id=rowid).one()):
|
||||||
|
return request.error(f'Cannot find password: {rowid}', 404)
|
||||||
except KeyError:
|
|
||||||
return request.error(f'Cannot find password: {rowid}', 404)
|
|
||||||
|
|
||||||
row.copy_password()
|
row.copy_password()
|
||||||
|
|
||||||
return request.ok_or_redirect(f'Copied password for 60 seconds')
|
return request.ok_or_redirect(f'Copied password for 60 seconds')
|
||||||
|
|
||||||
|
|
||||||
@Local.route('/passwords/delete/{rowid}')
|
@Local.route('/passwords/delete/{rowid}')
|
||||||
def passwords_update(handler, request, rowid):
|
def passwords_update(handler, request, rowid):
|
||||||
try:
|
with handler.db.session as s:
|
||||||
row = passdb[rowid]
|
try:
|
||||||
label = row.label
|
s.remove('passwords', id=rowid)
|
||||||
row.delete()
|
return request.ok_or_redirect(f'Deleted password: {label}')
|
||||||
return request.ok_or_redirect(f'Deleted password: {label}')
|
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return request.error(f'Cannot find password: {rowid}', 404)
|
return request.error(f'Cannot find password: {rowid}', 404)
|
||||||
|
|
||||||
|
|
||||||
### Preferences ###
|
### Preferences ###
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 6c71f895d8fc58a4cc58b593c8b7d2c509a7181e
|
|
10
webextension/meson.build
Normal file
10
webextension/meson.build
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
project('BarksharkWebExtension', 'c', 'vala', version: '0.1')
|
||||||
|
|
||||||
|
shared_library('extproxy',
|
||||||
|
'webextension.vala',
|
||||||
|
dependencies: [
|
||||||
|
dependency('webkit2gtk-web-extension-4.0'),
|
||||||
|
dependency('json-glib-1.0')
|
||||||
|
],
|
||||||
|
install: true
|
||||||
|
)
|
121
webextension/webextension.vala
Normal file
121
webextension/webextension.vala
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
private void handle_console_message_sent(WebKit.WebExtension extension, WebKit.ConsoleMessage message) {
|
||||||
|
var ext_message = new WebKit.UserMessage(
|
||||||
|
"console-message-sent",
|
||||||
|
new Variant("(ssi)", message.get_text(), message.get_level().to_string(), message.get_line())
|
||||||
|
);
|
||||||
|
|
||||||
|
extension.send_message_to_context.begin(ext_message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// private void handle_form_controls(WebKit.WebExtension extension, WebKit.WebPage page, GenericArray<WebKit.DOM.Element> elements, WebKit.Frame frame) {
|
||||||
|
// var data = new Json.Builder();
|
||||||
|
// data.begin_array();
|
||||||
|
//
|
||||||
|
// elements.foreach((elem) => {
|
||||||
|
// data.begin_object();
|
||||||
|
//
|
||||||
|
// data.set_member_name("tag");data.add_string_value(elem.tag_name);
|
||||||
|
// data.set_member_name("id");data.add_string_value(elem.id);
|
||||||
|
// data.set_member_name("name");data.add_string_value(elem.attributes.name);
|
||||||
|
//
|
||||||
|
// data.end_object();
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// data.end_array();
|
||||||
|
//
|
||||||
|
// var ext_message = new WebKit.UserMessage(
|
||||||
|
// "context-menu",
|
||||||
|
// new Variant("",
|
||||||
|
// new Variant("()", )
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// private async bool handle_send_request(WebKit.WebExtension extension, WebKit.URIRequest request, WebKit.URIResponse redirected_response) {
|
||||||
|
// var ext_message = new WebKit.UserMessage(
|
||||||
|
// "send-request",
|
||||||
|
// new Variant("(s)", request.get_uri())
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// var result = yield extension.send_message_to_context(ext_message, null);
|
||||||
|
// return result.get_parameters().get_boolean();
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
private void handle_form_submit(WebKit.WebExtension extension, WebKit.FormSubmissionStep stage, GenericArray<string> keys, GenericArray<string> values) {
|
||||||
|
var gen = new Json.Generator();
|
||||||
|
var data = new Json.Builder();
|
||||||
|
|
||||||
|
data.begin_object();
|
||||||
|
data.set_member_name("keys");
|
||||||
|
data.begin_array();
|
||||||
|
|
||||||
|
keys.foreach((key) => {
|
||||||
|
data.add_string_value(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.end_array();
|
||||||
|
|
||||||
|
data.set_member_name("values");
|
||||||
|
data.begin_array();
|
||||||
|
|
||||||
|
values.foreach((value) => {
|
||||||
|
data.add_string_value(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.end_array();
|
||||||
|
data.end_object();
|
||||||
|
|
||||||
|
gen.set_root(data.get_root());
|
||||||
|
|
||||||
|
var ext_message = new WebKit.UserMessage(
|
||||||
|
"form-sent",
|
||||||
|
new Variant("(ss)", stage.to_string(), gen.to_data(null))
|
||||||
|
);
|
||||||
|
|
||||||
|
extension.send_message_to_context.begin(ext_message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void handle_page_created(WebKit.WebExtension extension, WebKit.WebPage page) {
|
||||||
|
var ext_message = new WebKit.UserMessage(
|
||||||
|
"page-created",
|
||||||
|
new Variant("(t)", page.get_id())
|
||||||
|
);
|
||||||
|
|
||||||
|
extension.send_message_to_context.begin(ext_message, null);
|
||||||
|
|
||||||
|
page.console_message_sent.connect((message) => {
|
||||||
|
handle_console_message_sent(extension, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// page.context_menu.connect((ContextMenu menu, WebHitResult, hit_result) => {
|
||||||
|
// handle_context_menu(extension, menu, hit_result)
|
||||||
|
// });
|
||||||
|
|
||||||
|
// page.form_controls_associated_for_frame.connect((web_page, elements, frame) => {
|
||||||
|
// handle_form_controls(extension, web_page, elements, frame)
|
||||||
|
// });
|
||||||
|
|
||||||
|
// page.send_request.connect((request, redirected_response) => {
|
||||||
|
// return handle_send_request(extension, request, redirected_response);
|
||||||
|
// });
|
||||||
|
|
||||||
|
page.will_submit_form.connect((form, stage, source_frame, target_rame, field_names, field_values) => {
|
||||||
|
handle_form_submit(extension, stage, field_names, field_values);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void handle_message_recv(WebKit.WebExtension extension, WebKit.UserMessage message) {
|
||||||
|
print(message.get_name());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[CCode (cname="G_MODULE_EXPORT webkit_web_extension_initialize", has_target = false)]
|
||||||
|
public static void webkit_web_extension_initialize(WebKit.WebExtension extension) {
|
||||||
|
extension.page_created.connect(handle_page_created);
|
||||||
|
extension.user_message_received.connect(handle_message_recv);
|
||||||
|
}
|
Reference in a new issue