示例#1
0
 def run(self):
     while True:
         self.server.start_flag.wait()
         if self.server.kill_flag.is_set():
             break
         try:
             self.cache_timer.cancel()
         except AttributeError:
             pass
         self._set_timer()
         res = dmenu_run(self.entries, self.folders, self.collections,
                         self.session, self.prev_entry)
         if res == Run.LOCK:
             try:
                 self.server.kill_flag.set()
             except (EOFError, IOError):
                 return
         if res == Run.RELOAD:
             self.entries, self.folders, self.collections, self.orgs = \
                     bwcli.get_entries(self.session)
             if not all(i for i in (self.entries, self.folders, self.collections, self.orgs)
                        if i is False):
                 dmenu_err("Error loading entries. See logs.")
         if str(res) not in repr(Run.__members__):
             self.prev_entry = res or self.prev_entry
         if self.server.cache_time_expired.is_set():
             self.server.kill_flag.set()
         if self.server.kill_flag.is_set():
             break
         self.server.start_flag.clear()
示例#2
0
def type_entry(entry):
    """Pick which library to use to type strings

    Defaults to pynput

    Args: entry - dict

    """
    # Don't autotype anything except for login and cards - for now TODO
    if entry['type'] not in (1, 3):
        dmenu_err("Autotype currently disabled for this type of entry")
        return
    sequence = autotype_seq(entry)
    if sequence == 'False':
        dmenu_err("Autotype disabled for this entry")
        return
    if not sequence or sequence == 'None':
        sequence = SEQUENCE
        if entry['type'] == 3:
            sequence = "{CARDNUM}"
    tokens = tokenize_autotype(sequence)

    library = 'pynput'
    if CONF.has_option('vault', 'type_library'):
        library = CONF.get('vault', 'type_library')
    if library == 'xdotool':
        type_entry_xdotool(entry, tokens)
    elif library == 'ydotool':
        type_entry_ydotool(entry, tokens)
    else:
        type_entry_pynput(entry, tokens)
示例#3
0
def get_auth():
    """Generate and save port and authkey to ~/.cache/.bwm-auth

    Returns: int port, bytestring authkey

    """
    auth = bwm.configparser.ConfigParser()
    if not exists(bwm.AUTH_FILE):
        fdr = os.open(bwm.AUTH_FILE, os.O_WRONLY | os.O_CREAT, 0o600)
        with open(fdr, 'w', encoding=bwm.ENC) as a_file:
            auth.set('DEFAULT', 'port', str(find_free_port()))
            auth.set('DEFAULT', 'authkey', random_str())
            auth.write(a_file)
    try:
        auth.read(bwm.AUTH_FILE)
        port = auth.get('DEFAULT', 'port')
        authkey = auth.get('DEFAULT', 'authkey').encode()
    except (bwm.configparser.NoOptionError,
            bwm.configparser.MissingSectionHeaderError,
            bwm.configparser.ParsingError,
            multiprocessing.context.AuthenticationError):
        os.remove(bwm.AUTH_FILE)
        dmenu_err("Cache file was corrupted. Stopping all instances. Please try again")
        call(["pkill", "bwm"])  # Kill all prior instances as well
        return None, None
    return int(port), authkey
示例#4
0
def type_entry_ydotool(entry, tokens):
    """Auto-type entry entry using ydotool

    """
    enter_idx = True
    for token, special in tokens:
        if special:
            cmd = token_command(token)
            if callable(cmd):
                cmd()  # pylint: disable=not-callable
            elif token in PLACEHOLDER_AUTOTYPE_TOKENS:
                to_type = PLACEHOLDER_AUTOTYPE_TOKENS[token](entry)
                if to_type:
                    call(['ydotool', 'type', to_type])
            elif token in STRING_AUTOTYPE_TOKENS:
                to_type = STRING_AUTOTYPE_TOKENS[token]
                call(['ydotool', 'type', to_type])
            elif token in YDOTOOL_AUTOTYPE_TOKENS:
                cmd = ['ydotool'] + YDOTOOL_AUTOTYPE_TOKENS[token]
                call(cmd)
                # Add extra {ENTER} key tap for first instance of {ENTER}. It
                # doesn't get recognized for some reason.
                if enter_idx is True and token in ("{ENTER}", "~"):
                    cmd = ['ydotool'] + YDOTOOL_AUTOTYPE_TOKENS[token]
                    call(cmd)
                    enter_idx = False
            else:
                dmenu_err(f"Unsupported auto-type token (ydotool): {token}")
                return
        else:
            call(['ydotool', 'type', token])
示例#5
0
def get_password_chars():
    """Get characters to use for password generation from defaults, config file
    and user input.

    Returns: Dict {preset_name_1: {char_set_1: string, char_set_2: string},
                   preset_name_2: ....}
    """
    chars = {"upper": string.ascii_uppercase,
             "lower": string.ascii_lowercase,
             "digits": string.digits,
             "punctuation": string.punctuation}
    presets = {}
    presets["Letters+Digits+Punctuation"] = chars
    presets["Letters+Digits"] = {k: chars[k] for k in ("upper", "lower", "digits")}
    presets["Letters"] = {k: chars[k] for k in ("upper", "lower")}
    presets["Digits"] = {k: chars[k] for k in ("digits",)}
    if bwm.CONF.has_section('password_chars'):
        pw_chars = dict(bwm.CONF.items('password_chars'))
        chars.update(pw_chars)
        for key, val in pw_chars.items():
            presets[key.title()] = {k: chars[k] for k in (key,)}
    if bwm.CONF.has_section('password_char_presets'):
        if bwm.CONF.options('password_char_presets'):
            presets = {}
        for name, val in bwm.CONF.items('password_char_presets'):
            try:
                presets[name.title()] = {k: chars[k] for k in shlex.split(val)}
            except KeyError:
                dmenu_err(f"Error: Unknown value in preset {name}. Ignoring.")
                continue
    inp = "\n".join(presets)
    char_sel = dmenu_select(len(presets),
                            "Pick character set(s) to use", inp=inp)
    # This dictionary return also handles Rofi multiple select
    return {k: presets[k] for k in char_sel.split('\n')} if char_sel else False
示例#6
0
def dmenu_sync(session):
    """Call vault sync option (called from dmenu_run)

        Args: session (bytes)

    """
    res = bwcli.sync(session)
    if res is False:
        dmenu_err("Sync error. Check logs.")
示例#7
0
def delete_entry(entry, entries, session):
    """Delete an entry

    Args: entry - dict
          entries - list of dicts
          session - bytes

    """
    inp = "NO\nYes - confirm delete\n"
    delete = dmenu_select(2, f"Confirm delete of {entry['name']}", inp=inp)
    if delete != "Yes - confirm delete":
        return
    res = bwcli.delete_entry(entry, session)
    if res is False:
        dmenu_err("Item not deleted. Check logs.")
        return
    del entries[entries.index(res)]
示例#8
0
def rename_folder(folders, session):
    """Rename folder

    Args: folders - dict {'name': folder dict, ...}

    """
    folder = select_folder(folders, prompt="Select folder to rename")
    if folder is False or folder['name'] == "No Folder":
        return
    name = dmenu_select(1, "New folder name", inp=basename(folder['name']))
    if not name:
        return
    new = join(dirname(folder['name']), name)
    folder = bwcli.move_folder(folder, new, session)
    if folder is False:
        dmenu_err("Folder not renamed. Check logs.")
        return
    folders[folder['id']] = folder
示例#9
0
def type_text(data):
    """Type the given text data

    """
    library = 'pynput'
    if CONF.has_option('vault', 'type_library'):
        library = CONF.get('vault', 'type_library')
    if library == 'xdotool':
        call(['xdotool', 'type', data])
    elif library == 'ydotool':
        call(['ydotool', 'type', data])
    else:
        kbd = keyboard.Controller()
        try:
            kbd.type(data)
        except kbd.InvalidCharacterException:
            dmenu_err("Unable to type string...bad character.\n"
                      "Try setting `type_library = xdotool` in config.ini")
示例#10
0
def get_initial_vault():
    """Ask for initial server URL and email if not entered in config file

    """
    url = dmenu_select(0, "Enter server URL.", "https://vault.bitwarden.com")
    if not url:
        dmenu_err("No URL entered. Try again.")
        return False
    email = dmenu_select(0, "Enter login email address.")
    twofa = {'None': '', 'TOTP': 0, 'Email': 1, 'Yubikey': 3}
    method = dmenu_select(len(twofa), "Select Two Factor Auth type.", "\n".join(twofa))
    with open(bwm.CONF_FILE, 'w', encoding=bwm.ENC) as conf_file:
        bwm.CONF.set('vault', 'server_1', url)
        if email:
            bwm.CONF.set('vault', 'email_1', email)
        if method:
            bwm.CONF.set('vault', 'twofactor_1', str(twofa[method]))
        bwm.CONF.write(conf_file)
    return (url, email, '', twofa[method])
示例#11
0
def rename_collection(collections, session):
    """Rename collection

    Args: collections - dict {'name': collection dict, ...}

    """
    collection = select_collection(collections, session, prompt="Select collection to rename")
    if not collection:
        return
    collection = next(iter(collection.values()))
    name = dmenu_select(1, "New collection name", inp=basename(collection['name']))
    if not name:
        return
    new = join(dirname(collection['name']), name)
    res = bwcli.move_collection(collection, new, session)
    if res is False:
        dmenu_err("Collection not deleted. Check logs.")
        return
    collections[collection['id']] = res
示例#12
0
def edit_password(entry):  # pylint: disable=too-many-return-statements
    """Edit password

        Args: entry dict
        Returns: entry dict

    """
    sel = entry['login']['password']
    pw_orig = sel + "\n" if sel is not None else "\n"
    inputs = ["Generate password",
              "Manually enter password"]
    if entry['login']['password']:
        inputs.append("Type existing password")
    pw_choice = dmenu_select(len(inputs), "Password Options", inp="\n".join(inputs))
    if pw_choice == "Manually enter password":
        sel = dmenu_select(1, "Password", inp=pw_orig)
        sel_check = dmenu_select(1, "Verify password")
        if not sel_check or sel_check != sel:
            dmenu_err("Passwords do not match. No changes made.")
            return False
    elif pw_choice == "Generate password":
        inp = "20\n"
        length = dmenu_select(1, "Password Length?", inp=inp)
        if not length:
            return False
        try:
            length = int(length)
        except ValueError:
            length = 20
        chars = get_password_chars()
        if chars is False:
            return False
        sel = gen_passwd(chars, length)
        if sel is False:
            dmenu_err("Number of char groups desired is more than requested pw length")
            return False
    elif pw_choice == "Type existing password":
        type_text(entry['login']['password'])
        return False
    else:
        return False
    entry['login']['password'] = sel
    return entry
示例#13
0
def delete_folder(folders, session):
    """Delete a folder

    Args: folder - folder dict obj
          session - bytes

    """
    folder = select_folder(folders, prompt="Delete Folder:")
    if not folder or folder['name'] == "No Folder":
        return
    inp = "NO\nYes - confirm delete\n"
    delete = dmenu_select(2, "Confirm delete", inp=inp)
    if delete != "Yes - confirm delete":
        return
    res = bwcli.delete_folder(folder, session)
    if res is False:
        dmenu_err("Folder not deleted. Check logs.")
        return
    del folders[folder['id']]
示例#14
0
def delete_collection(collections, session):
    """Delete a collection

    Args: collections- dict of all collection objects
          session - bytes

    """
    collection = select_collection(collections, session, prompt="Delete collection:")
    if not collection:
        return
    collection = next(iter(collection.values()))
    inp = "NO\nYes - confirm delete\n"
    delete = dmenu_select(2, f"Confirm delete of {collection['name']}", inp=inp)
    if delete != "Yes - confirm delete":
        return
    res = bwcli.delete_collection(collection, session)
    if res is False:
        dmenu_err("Collection not deleted. Check logs.")
        return
    del collections[collection['id']]
示例#15
0
def move_folder(folders, session):
    """Move folder

    Args: folders - dict {'name': folder dict, ...}

    """
    folder = select_folder(folders, prompt="Select folder to move")
    if folder is False or folder['name'] == "No Folder":
        return
    destfolder = select_folder(folders,
                               prompt="Select destination folder. 'No Folder' is root.")
    if destfolder is False:
        return
    dname = ""
    if destfolder['name'] != "No Folder":
        dname = destfolder['name']
    folder = bwcli.move_folder(folder, join(dname, basename(folder['name'])), session)
    if folder is False:
        dmenu_err("Folder not added. Check logs.")
        return
    folders[folder['id']] = folder
示例#16
0
def create_folder(folders, session):
    """Create new folder

    Args: folders - dict of folder objects

    """
    parentfolder = select_folder(folders, prompt="Select parent folder")
    if parentfolder is False:
        return
    pfname = parentfolder['name']
    if pfname == "No Folder":
        pfname = ""
    name = dmenu_select(1, "Folder name")
    if not name:
        return
    name = join(pfname, name)
    folder = bwcli.add_folder(name, session)
    if folder is False:
        dmenu_err("Folder not added. Check logs.")
        return
    folders[folder['id']] = folder
示例#17
0
def edit_notes(note):
    """Use $EDITOR (or 'vim' if not set) to edit the notes entry

    In configuration file:
        Set 'gui_editor' for things like emacs, gvim, leafpad
        Set 'editor' for vim, emacs -nw, nano unless $EDITOR is defined
        Set 'terminal' if using a non-gui editor

    Args: note - string
    Returns: note - string

    """
    if bwm.CONF.has_option("vault", "gui_editor"):
        editor = bwm.CONF.get("vault", "gui_editor")
        editor = shlex.split(editor)
    else:
        if bwm.CONF.has_option("vault", "editor"):
            editor = bwm.CONF.get("vault", "editor")
        else:
            editor = os.environ.get('EDITOR', 'vim')
        if bwm.CONF.has_option("vault", "terminal"):
            terminal = bwm.CONF.get("vault", "terminal")
        else:
            terminal = "xterm"
        terminal = shlex.split(terminal)
        editor = shlex.split(editor)
        editor = terminal + ["-e"] + editor
    note = b'' if note is None else note.encode(bwm.ENC)
    with tempfile.NamedTemporaryFile(suffix=".tmp") as fname:
        fname.write(note)
        fname.flush()
        editor.append(fname.name)
        try:
            call(editor)
            fname.seek(0)
            note = fname.read()
        except FileNotFoundError:
            dmenu_err("Terminal not found. Please update config.ini.")
    note = '' if not note else note.decode(bwm.ENC)
    return note
示例#18
0
def tokenize_autotype(autotype):
    """Process the autotype sequence

    Args: autotype - string
    Returns: tokens - generator ((token, if_special_char T/F), ...)

    """
    while autotype:
        opening_idx = -1
        for char in "{+^%~@":
            idx = autotype.find(char)
            if idx != -1 and (opening_idx == -1 or idx < opening_idx):
                opening_idx = idx

        if opening_idx == -1:
            # found the end of the string without further opening braces or
            # other characters
            yield autotype, False
            return

        if opening_idx > 0:
            yield autotype[:opening_idx], False

        if autotype[opening_idx] in "+^%~@":
            yield autotype[opening_idx], True
            autotype = autotype[opening_idx + 1:]
            continue

        closing_idx = autotype.find('}')
        if closing_idx == -1:
            dmenu_err("Unable to find matching right brace (}) while" +
                      f"tokenizing auto-type string: {autotype}\n")
            return
        if closing_idx == opening_idx + 1 and closing_idx + 1 < len(autotype) \
                and autotype[closing_idx + 1] == '}':
            yield "{}}", True
            autotype = autotype[closing_idx + 2:]
            continue
        yield autotype[opening_idx:closing_idx + 1], True
        autotype = autotype[closing_idx + 1:]
示例#19
0
def type_entry_pynput(entry, tokens):  # pylint: disable=too-many-branches
    """Use pynput to auto-type the selected entry

    """
    kbd = keyboard.Controller()
    enter_idx = True
    for token, special in tokens:
        if special:
            cmd = token_command(token)
            if callable(cmd):
                cmd()  # pylint: disable=not-callable
            elif token in PLACEHOLDER_AUTOTYPE_TOKENS:
                to_type = PLACEHOLDER_AUTOTYPE_TOKENS[token](entry)
                if to_type:
                    try:
                        kbd.type(to_type)
                    except kbd.InvalidCharacterException:
                        dmenu_err("Unable to type string...bad character.\n"
                                  "Try setting `type_library = xdotool` in config.ini")
                        return
            elif token in STRING_AUTOTYPE_TOKENS:
                to_type = STRING_AUTOTYPE_TOKENS[token]
                try:
                    kbd.type(to_type)
                except kbd.InvalidCharacterException:
                    dmenu_err("Unable to type string...bad character.\n"
                              "Try setting `type_library = xdotool` in config.ini")
                    return
            elif token in PYNPUT_AUTOTYPE_TOKENS:
                to_tap = PYNPUT_AUTOTYPE_TOKENS[token]
                kbd.tap(to_tap)
                # Add extra {ENTER} key tap for first instance of {ENTER}. It
                # doesn't get recognized for some reason.
                if enter_idx is True and token in ("{ENTER}", "~"):
                    kbd.tap(to_tap)
                    enter_idx = False
            else:
                dmenu_err(f"Unsupported auto-type token (pynput): {token}")
                return
        else:
            try:
                kbd.type(token)
            except kbd.InvalidCharacterException:
                dmenu_err("Unable to type string...bad character.\n"
                          "Try setting `type_library = xdotool` in config.ini")
                return
示例#20
0
def move_collection(collections, session):
    """Move collection

    Args: collections - dict {'name': collection dict, ...}

    """
    collection = select_collection(collections, session, prompt="Select collection to move")
    if not collection:
        return
    collection = next(iter(collection.values()))
    destcollection = select_collection(collections, session,
                                       prompt="Select destination collection "
                                              "(Esc to move to root directory)")
    if not destcollection:
        destcollection = {'name': ""}
    else:
        destcollection = next(iter(destcollection.values()))
    res = bwcli.move_collection(collection,
                                join(destcollection['name'], basename(collection['name'])),
                                session)
    if res is False:
        dmenu_err("Collection not moved. Check logs.")
        return
    collections[collection['id']] = res
示例#21
0
        CONF.add_section('dmenu_passphrase')
        CONF.set('dmenu_passphrase', 'obscure', 'True')
        CONF.set('dmenu_passphrase', 'obscure_color', '#222222')
        CONF.add_section('vault')
        CONF.set('vault', 'server_1', '')
        CONF.set('vault', 'email_1', '')
        CONF.set('vault', 'twofactor_1', '')
        CONF.set('vault', 'session_timeout_min ',
                 str(SESSION_TIMEOUT_DEFAULT_MIN))
        CONF.set('vault', 'autotype_default', SEQUENCE)
        CONF.write(conf_file)
CONF = configparser.ConfigParser()
try:
    CONF.read(CONF_FILE)
except configparser.ParsingError as err:
    dmenu_err(f"Config file error: {err}")
    sys.exit()
if CONF.has_option('dmenu', 'dmenu_command'):
    command = shlex.split(CONF.get('dmenu', 'dmenu_command'))
if "-l" in command:
    MAX_LEN = int(command[command.index("-l") + 1])
else:
    MAX_LEN = 24
if CONF.has_option("vault", "session_timeout_min"):
    SESSION_TIMEOUT_MIN = int(CONF.get("vault", "session_timeout_min"))
else:
    SESSION_TIMEOUT_MIN = SESSION_TIMEOUT_DEFAULT_MIN
if CONF.has_option('vault', 'autotype_default'):
    SEQUENCE = CONF.get("vault", "autotype_default")
if CONF.has_option("vault", "type_library"):
    if CONF.get("vault", "type_library") == "xdotool":
示例#22
0
def edit_entry(entry, entries, folders, collections, session):
    # pylint: disable=too-many-branches,too-many-statements,too-many-locals
    """Edit title, username, password, url, notes and autotype sequence for an entry.

    Args: entry - selected Entry dict
          entries - list of dicts
          folders - dict of dicts {'id': {xxx,yyy}, ... }
          collections - dict of dicts {'id': {xxx,yyy}, ... }
          session - bytes
    Returns: None or entry (Item)

    """
    item = deepcopy(entry)
    update_colls = "NO"
    while True:
        fields = [str("Name: {}").format(item['name']),
                  str("Folder: {}").format(obj_name(folders, item['folderId'])),
                  str("Collections: {}").format(", ".join(obj_name(collections, i) for i
                                                          in item['collectionIds'])),
                  str("Username: {}").format(item['login']['username']),
                  str("Password: **********") if item['login']['password'] else "Password: None",
                  str("Url: {}").format(item['login']['url']),
                  str("Autotype: {}").format(autotype_seq(item)),
                  "Notes: <Enter to Edit>" if item['notes'] else "Notes: None",
                  "Delete entry",
                  "Save entry"]
        inp = "\n".join(fields)
        sel = dmenu_select(len(fields), inp=inp)
        if sel == 'Delete entry':
            delete_entry(entry, entries, session)
            return None
        if sel == 'Save entry':
            if not item.get('id'):
                res = bwcli.add_entry(item, session)
                if res is False:
                    dmenu_err("Entry not added. Check logs.")
                    return None
                entries.append(bwcli.Item(res))
            else:
                res = bwcli.edit_entry(item, session, update_colls)
                if res is False:
                    dmenu_err("Error saving entry. Changes not saved.")
                    continue
                entries[entries.index(entry)] = bwcli.Item(res)
            return bwcli.Item(res)
        try:
            field, sel = sel.split(": ", 1)
        except (ValueError, TypeError):
            return entry
        field = field.lower()
        if field == 'password':
            item = edit_password(item) or item
            continue
        if field == 'folder':
            folder = select_folder(folders)
            if folder is not False:
                item['folderId'] = folder['id']
            continue
        if field == 'collections':
            orig = item['collectionIds']
            coll_list = [collections[i] for i in collections if i in item['collectionIds']]
            collection = select_collection(collections, session, coll_list=coll_list)
            item['collectionIds'] = [*collection]
            if collection:
                item['organizationId'] = next(iter(collection.values()))['organizationId']
            if item['collectionIds'] and item['collectionIds'] != orig and orig:
                update_colls = "YES"
            elif item['collectionIds'] != orig and not orig:
                update_colls = "MOVE"
            elif not item['collectionIds'] and orig:
                update_colls = "REMOVE"
            continue
        if field == 'notes':
            item['notes'] = edit_notes(item['notes'])
            continue
        if field in ('username', 'url'):
            edit = item['login'][field] + \
                    "\n" if item['login'][field] is not None else "\n"
        elif field == 'autotype':
            edit = item['fields'][autotype_index(item)]['value'] + \
                    "\n" if item['fields'][autotype_index(item)]['value'] is not None else "\n"
        else:
            edit = item[field] + "\n" if item[field] is not None else "\n"
        sel = dmenu_select(1, f"{field.capitalize()}", inp=edit)
        if sel:
            if field in ('username', 'url'):
                item['login'][field] = sel
                if field == 'url':
                    item['login']['uris'] = [{'match': None, 'uri': sel}]
            elif field == 'autotype':
                item['fields'][autotype_index(item)]['value'] = sel
            else:
                item[field] = sel
示例#23
0
def get_vault():
    # pylint: disable=too-many-return-statements,too-many-locals,too-many-branches
    """Read vault login parameters from config or ask for user input.

    Returns: Session - bytes
                       None on error opening/reading vault

    """
    args = bwm.CONF.items('vault')
    args_dict = dict(args)
    servers = [i for i in args_dict if i.startswith('server')]
    vaults = []
    for srv in servers:
        idx = srv.rsplit('_', 1)[-1]
        email = args_dict.get(f'email_{idx}', "")
        passw = args_dict.get(f'password_{idx}', "")
        twofactor = args_dict.get(f'twofactor_{idx}', None)
        if not args_dict[srv] or not email:
            continue
        try:
            cmd = args_dict[f'password_cmd_{idx}']
            res = subprocess.run(shlex.split(cmd),
                                 check=False,
                                 capture_output=True,
                                 encoding=bwm.ENC)
            if res.stderr:
                dmenu_err(f"Password command error: {res.stderr}")
                sys.exit()
            else:
                passw = res.stdout.rstrip('\n') if res.stdout else passw
        except KeyError:
            pass
        vaults.append((args_dict[srv], email, passw, twofactor))
    if not vaults or (not vaults[0][0] or not vaults[0][1]):
        res = get_initial_vault()
        if res:
            vaults.insert(0, res)
        else:
            return None
    if len(vaults) > 1:
        inp = "\n".join(i[0] for i in vaults)
        sel = dmenu_select(len(vaults), "Select Vault", inp=inp)
        vaults = [i for i in vaults if i[0] == sel]
        if not sel or not vaults:
            return None
    url, email, passw, twofactor = vaults[0]
    status = bwcli.status()
    if status is False:
        return None
    if not passw:
        passw = get_passphrase()
        if not passw:
            return None
    if status['serverUrl'] != url:
        res = bwcli.set_server(url)
        if res is False:
            return None
    if status['userEmail'] != email or status['status'] == 'unauthenticated':
        code = get_passphrase(True) if twofactor else ""
        session, err = bwcli.login(email, passw, twofactor, code)
    elif status['status'].endswith('locked'):
        session, err = bwcli.unlock(passw)
    if session is False:
        dmenu_err(err)
        return None
    return session