This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
barkshark-web/barkshark_web/passwords/bitwarden/storage.py

440 lines
9.2 KiB
Python

import time
from datetime import datetime, timedelta
from izzylib import logging
from izzylib.datestring import DateString
from izzylib.exceptions import HttpClientError
from izzylib.http_client import HttpClient
from izzylib.misc import convert_to_boolean, convert_to_string, random_port
from json.decoder import JSONDecodeError
from nodejs import node, npm
from .item import BitwardenItem
from .result import BitwardenResult
from ..base import PasswordItem, PasswordStorage
from ... import scriptpath
from ...enums import *
from ...exceptions import BitwardenApiError, NoPasswordError
bwbin = scriptpath.join('bin/bw')
bwjs = scriptpath.parent.join('node_modules/@bitwarden/cli/build/bw.js')
def bool_str(value):
if convert_to_boolean(value):
return 'true'
return 'false'
def check_deps():
if not bwjs.exists():
npm.run(['install', '@bitwarden/cli'])
class BitwardenStorage(PasswordStorage):
item_class = BitwardenItem
result_class = BitwardenResult
def __init__(self, host='localhost', port=None, external=False, session_key=None):
self._proc = None
self._account = None
self._current_port = None
self._client = HttpClient(headers={
'Accept': 'application/json',
'Content-Type': 'application/json'
})
self.host = host
self.port = port
self.external = external
self.session_key = session_key
if not self.port:
if external:
self.port = 8087
else:
self.port = random_port(8000, 16000)
check_deps()
def __del__(self):
self.stop()
@property
def api_active(self):
return self.external or (self._proc and self._proc.poll() == None)
@property
def unlocked(self):
return True if self.session_key else False
@property
def account(self):
if not self._account:
self._account = self.status().userEmail
return self._account
## broken
#return self.status().status == 'unlocked'
def _process_response(self, raw_data, return_raw=False):
try:
data = DotDict(raw_data)
if return_raw:
return data
if 'success' not in data:
raise BitwardenApiError(data.message)
try:
return data.data
except AttributeError:
return data
except JSONDecodeError as e:
print(raw_data)
raise e from None
def get_port(self):
if self.port:
return self.port
if not self._current_port:
if self.external:
self._current_port = 8087
else:
self._current_port = random_port(8000, 16000)
return self._current_port
def create_row(self, *args, **kwargs):
return self.item_class.new(*args, **kwargs, storage=self)
## send cli command
def execute(self, command, *args, data=None, key=True, return_raw=False):
#cmd = [bwbin, '--response']
cmd = [bwjs, '--response']
if key:
if not self.session_key:
raise ValueError('Missing session key. Try calling login first.')
cmd.append('--session')
cmd.append(self.session_key)
proc_kwargs = {
'capture_output': True,
'encoding': 'utf-8'
}
if data:
proc_kwargs['input'] = convert_to_string(data)
result = node.run([*cmd, command, *args], **proc_kwargs)
return self._process_response(result.stdout, return_raw)
## send api command
def send(self, api, method, data=None, return_raw=False):
if not self.api_active:
raise ConnectionError('API server is not running')
kwargs = {'method': method}
url = Url.new(self.host,
port = self.get_port(),
path = api,
proto = 'http'
)
if data and method == 'GET':
url = url.replace_property('query', data)
with self._client.request(url, method, data, raise_error=True) as resp:
return self._process_response(resp.text, return_raw)
## api server
def start(self, password=None):
if self.external or self.api_active:
return
if password:
self.unlock(password)
if not self.session_key:
raise ValueError('Missing session key')
cmd = [bwjs, 'serve']
cmd.extend(['--hostname', self.host])
cmd.extend(['--port', str(self.port)])
cmd.extend(['--session', self.session_key])
self._proc = node.Popen(cmd)
while True:
try:
self.account
return
except ConnectionRefusedError:
time.sleep(0.1)
def stop(self, lock=False):
if self.external or not self.api_active:
return
self._proc.terminate()
wait_time = datetime.now()
while self._proc.poll() == None:
if wait_time + timedelta(seconds=5) >= datetime.now():
self._proc.kill()
break
if lock:
self.lock()
self._proc = None
self._account = None
self._current_port = None
## command methods
def login(self, email, password):
data = self.execute('login', email, password, key=False)
self.session_key = data.raw
return data.raw
def logout(self):
self.execute('logout')
self.session_key = None
def lock(self):
self.execute('lock')
self.session_key = None
def unlock(self, password):
data = self.execute('unlock', password)
self.session_key = data.raw
return data.raw
## api methods
def status(self):
if self.session_key:
data = self.send('status', 'GET')
else:
data = self.execute('status', key=None)
return data.template
def fetch(self, text=None, trash=False, **fields):
kwargs = {}
if (row_id := fields.get('id')):
row = self.send(f'object/item/{row_id}', 'GET')
return self.item_class(self, row)
if text:
kwargs['search'] = text
if trash:
kwargs['trash'] = trash
rows = []
data = self.send('list/object/items', 'GET', kwargs)
for row in data.data:
row = self.item_class(self, row)
if row.compare(**fields):
rows.append(row)
return self.result_class(self, rows)
def insert(self, *args, **kwargs):
self.insert_row(self.item_class.new(*args, **kwargs, storage=self))
def insert_row(self, row):
data = self.execute('create', 'item',
self.execute('encode', data=row.to_json()).data
)
return self.item_class(self, data)
## Causes the http server to return a 500 error
def insert_row_api(self, row):
data = self.send('object/item', 'POST', row._data)
return self.item_class(self, data)
def update(self, item_id, **kwargs):
row = self.fetch(id=item_id)
new_row = self.update_row(row, **kwargs)
return new_row
def update_row(self, row, **kwargs):
row.update(kwargs)
new_row = self.send(f'object/item/{row.id}', 'PUT', row.to_json())
return self.item_class(self, new_row)
def remove(self, item_id):
data = self.send(f'object/item/{item_id}', 'DELETE', return_raw=True)
if not data.get('success'):
raise BitwardenApiError(data)
return True
def purge(self, item_id):
data = self.execute('delete', 'item', item_id, '--permanent')
if not data.get('success'):
raise BitwardenApiError(data)
return True
def purge_all(self, dry_run=False):
rows = self.fetch(trash=True).all()
for row in rows:
if not dry_run:
self.purge(row.id)
return rows
def restore(self, item_id):
data = self.send(f'restore/item/{item_id}', 'POST', return_raw=True)
if not data.get('success'):
raise BitwardenApiError(data)
return True
def passgen(self, passphrase=False, **kwargs):
if passphrase:
if 'separator' in kwargs:
assert len(kwargs['separator']) == 1, 'Separator must be exactly 1 character'
data = dict(
passphrase = 'true',
words = int(kwargs.get('words', 4)),
capitalize = bool_str(kwargs.get('capitalize')),
includeNumber = bool_str(kwargs.get('numbers')),
separator = kwargs.get('separator', '-')
)
else:
data = dict(
length = int(kwargs.get('length', 20)),
uppercase = bool_str(kwargs.get('uppercase', True)),
lowercase = bool_str(kwargs.get('lowercase', True)),
number = bool_str(kwargs.get('numbers', True)),
special = bool_str(kwargs.get('special', True))
)
return self.send('generate', 'GET', data).data
def generate_passphrase(self, words=4, capitalize=False, numbers=False, separator='-'):
return self.passgen(
passphrase = True,
words = words,
capitalize = capitalize,
numbers = numbers,
separator = separator
)
def generate_password(self, length=20, uppercase=True, lowercase=True, numbers=True, special=True):
return self.passgen(
passphrase = False,
length = length,
uppercase = uppercase,
lowercase = lowercase,
numbers = numbers,
special = special
)
def template(self, name, api=False):
if api:
return self.send(f'template/{name}', 'GET').template
return self.execute('get', 'template', name).template
def get_template(self, type='login'):
if type == 'login':
template = self.template('item')
template.login = self.template('item.login')
template.login.uris = [self.template('item.login.uri')]
template.fields = [self.template('item.field')]
elif type == 'card':
template = self.template('item')
template.card = self.template('item.card')
template.fields = []
elif type == 'identity':
template = self.template('item')
template.identity = self.template('item.identity')
template.fields = []
elif type == 'note':
template = self.template('item')
template.securenote = self.template('item.securenote')
template.fields = []
elif type == 'folder':
template = self.template('folder')
elif type == 'collection':
template = self.template('collection')
else:
raise ValueError(f'Not a valid template: {type}')
return template
def sync(self):
data = self.send('sync', 'POST', return_raw=True)
return data.success