Beispiel #1
0
def manage_collections(collections, session):
    """Rename, create, move or delete collections

    Args: collections - dict of collection objects {'name': dict, ...}
          session - bytes

    """
    options = ['Create',
               'Move',
               'Rename',
               'Delete']
    while True:
        inp = "\n".join(i for i in options) + "\n\n" + \
                "\n".join(i['name'] for i in collections.values())
        sel = dmenu_select(len(options) + len(collections) + 1, "Manage collections", inp=inp)
        if not sel:
            break
        if sel == 'Create':
            create_collection(collections, session)
        elif sel == 'Move':
            move_collection(collections, session)
        elif sel == 'Rename':
            rename_collection(collections, session)
        elif sel == 'Delete':
            delete_collection(collections, session)
        else:
            break
Beispiel #2
0
def manage_folders(folders, session):
    """Rename, create, move or delete folders

    Args: folders - dict of folder objects {'id': dict, ...}
          session - bytes

    """
    options = ['Create',
               'Move',
               'Rename',
               'Delete']
    while True:
        inp = "\n".join(i for i in options) + "\n\n" + \
            "\n".join(i['name'] for i in folders.values())
        sel = dmenu_select(len(options) + len(folders) + 1, "Manage Folders", inp=inp)
        if not sel:
            break
        if sel == 'Create':
            create_folder(folders, session)
        elif sel == 'Move':
            move_folder(folders, session)
        elif sel == 'Rename':
            rename_folder(folders, session)
        elif sel == 'Delete':
            delete_folder(folders, session)
        else:
            break
Beispiel #3
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
Beispiel #4
0
def view_ident(entry, folders):
    """Show title, identify information and notes for an identity entry.

    Returns: dmenu selection

    """
    fields = [
        entry['name'] or "Title: None",
        obj_name(folders, entry['folderId']), entry['identity']['title']
        or "Title: None", entry['identity']['firstName'] or "First name: None",
        entry['identity']['middleName'] or "Middle name: None",
        entry['identity']['lastName'] or "Last name: None",
        entry['identity']['address1'] or "Address1: None",
        entry['identity']['address2'] or "Address2: None",
        entry['identity']['address3'] or "Address3: None",
        entry['identity']['city'] or "City: None", entry['identity']['state']
        or "State: None", entry['identity']['postalCode']
        or "Postal Code: None", entry['identity']['country']
        or "Country: None", entry['identity']['email'] or "Email: None",
        entry['identity']['phone'] or "Phone: None", entry['identity']['ssn']
        or "SSN: None", entry['identity']['username'] or "Username: None",
        entry['identity']['passportNumber'] or "Passport #: None",
        entry['identity']['licenseNumber'] or "License #: None",
        "Notes: <Enter to view>" if entry['notes'] else "Notes: None"
    ]
    vault_entries = "\n".join(fields)
    sel = dmenu_select(len(fields), inp=vault_entries)
    if sel == "Notes: <Enter to view>":
        sel = view_notes(entry['notes'])
    elif sel == "Notes: None":
        sel = ""
    return sel
Beispiel #5
0
def view_all_entries(options, vault_entries, folders):
    """Generate numbered list of all vault entries and open with dmenu.

    Returns: dmenu selection

    """
    num_align = len(str(len(vault_entries)))
    # Login: Num(l) - Folder/name - username - url
    bw_login_pattern = str("{:>{na}}(l) - {} - {} - {}")
    # Secure Note: Num(n) - Folder/name
    bw_note_pattern = str("{:>{na}}(n) - {}")
    # Card: Num(c) - Folder/name - card type - card owner - card #
    bw_card_pattern = str("{:>{na}}(c) - {} - {} - {} - {}")
    # Identity: Num(i) - Folder/name - lastName, firstName - email - phone
    bw_ident_pattern = str("{:>{na}}(i) - {} - {}, {} - {} - {}")
    # Have to number each entry to capture duplicates correctly
    ven = []
    for j, i in enumerate(vault_entries):
        if i['type'] == 1:
            ven.append(
                bw_login_pattern.format(j,
                                        join(obj_name(folders, i['folderId']),
                                             i['name']),
                                        i['login']['username'],
                                        i['login']['url'],
                                        na=num_align))
        elif i['type'] == 2:
            ven.append(
                bw_note_pattern.format(j,
                                       join(obj_name(folders, i['folderId']),
                                            i['name']),
                                       na=num_align))
        elif i['type'] == 3:
            ven.append(
                bw_card_pattern.format(j,
                                       join(obj_name(folders, i['folderId']),
                                            i['name']),
                                       i['card']['brand'],
                                       i['card']['cardholderName'],
                                       i['card']['number'],
                                       na=num_align))
        elif i['type'] == 4:
            ven.append(
                bw_ident_pattern.format(j,
                                        join(obj_name(folders, i['folderId']),
                                             i['name']),
                                        i['identity']['lastName'],
                                        i['identity']['firstName'],
                                        i['identity']['email'],
                                        i['identity']['phone'],
                                        na=num_align))
    vault_entries_s = str("\n").join(ven)
    if options:
        options_s = ("\n".join(options) + "\n")
        entries_s = options_s + vault_entries_s
    else:
        entries_s = vault_entries_s
    return dmenu_select(min(bwm.MAX_LEN,
                            len(options) + len(vault_entries)),
                        inp=entries_s)
Beispiel #6
0
def get_passphrase(twofac=False):
    """Get a vault password from dmenu or pinentry

        Args: twofac - bool (default False) prompt for 2FA code
        Returns: string

    """
    pinentry = None
    pin_prompt = 'setdesc Enter vault password\ngetpin\n' if twofac is False \
        else 'setdesc Enter 2FA code\ngetpin\n'
    if bwm.CONF.has_option("dmenu", "pinentry"):
        pinentry = bwm.CONF.get("dmenu", "pinentry")
    if pinentry:
        password = ""
        out = subprocess.run(pinentry,
                             capture_output=True,
                             check=False,
                             encoding=bwm.ENC,
                             input=pin_prompt).stdout
        if out:
            res = out.split("\n")[2]
            if res.startswith("D "):
                password = res.split("D ")[1]
    else:
        password = dmenu_select(0, "Password" if twofac is False else "2FA Code")
    return password
Beispiel #7
0
def view_login(entry, folders):
    """Show title, username, password, url and notes for a login entry.

    Returns: dmenu selection

    """
    fields = [
        entry['name'] or "Title: None",
        obj_name(folders, entry['folderId']), entry['login']['username']
        or "Username: None",
        '**********' if entry['login']['password'] else "Password: None",
        entry['login']['url'] or "URL: None",
        "Notes: <Enter to view>" if entry['notes'] else "Notes: None"
    ]
    vault_entries = "\n".join(fields)
    sel = dmenu_select(len(fields), inp=vault_entries)
    if sel == "Notes: <Enter to view>":
        sel = view_notes(entry['notes'])
    elif sel == "Notes: None":
        sel = ""
    elif sel == '**********':
        sel = entry['login']['password']
    elif sel == fields[4]:
        if sel != "URL: None":
            webbrowser.open(sel)
        sel = ""
    return sel if not sel.endswith(": None") else ""
Beispiel #8
0
def view_notes(notes):
    """View the 'Notes' field line-by-line within dmenu.

    Returns: text of the selected line for typing

    """
    sel = dmenu_select(min(bwm.MAX_LEN, len(notes.split('\n'))), inp=notes)
    return sel
Beispiel #9
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
Beispiel #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])
Beispiel #11
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)]
Beispiel #12
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
Beispiel #13
0
def view_note(entry, folders):
    """Show title and note for a secure note entry.

    Returns: dmenu selection

    """
    fields = [
        entry['name'] or "Title: None",
        obj_name(folders, entry['folderId']),
        "Notes: <Enter to view>" if entry['notes'] else "Notes: None"
    ]
    vault_entries = "\n".join(fields)
    sel = dmenu_select(len(fields), inp=vault_entries)
    if sel == "Notes: <Enter to view>":
        sel = view_notes(entry['notes'])
    elif sel == "Notes: None":
        sel = ""
    return sel
Beispiel #14
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
Beispiel #15
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']]
Beispiel #16
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']]
Beispiel #17
0
def create_collection(collections, session):
    """Create new collection

    Args: collections - dict of collection objects

    """
    org_id = select_org(session)
    if org_id is False:
        return
    parentcollection = select_collection(collections, session,
                                         prompt="Select parent collection (Esc for no parent)")
    pname = ""
    if parentcollection:
        pname = next(iter(parentcollection.values()))['name']
    name = dmenu_select(1, "Collection name")
    if not name:
        return
    name = join(pname, name)
    collection = bwcli.add_collection(name, org_id['id'], session)
    collections[collection['id']] = collection
Beispiel #18
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
Beispiel #19
0
def select_org(session):
    """Select organization

    Args: session - bytes

    Returns: False for no entry
             org - dict

    """
    orgs = bwcli.get_orgs(session)
    orgs_ids = dict(enumerate(orgs.values()))
    num_align = len(str(len(orgs)))
    pattern = str("{:>{na}} - {}")
    inp = str("\n").join(pattern.format(j, i['name'], na=num_align)
                         for j, i in orgs_ids.items())
    sel = dmenu_select(min(bwm.MAX_LEN, len(orgs)), "Select Organization", inp=inp)
    if not sel:
        return False
    try:
        return orgs_ids[int(sel.split(' - ', 1)[0])]
    except (ValueError, TypeError):
        return False
Beispiel #20
0
def select_folder(folders, prompt="Folders"):
    """Select which folder for an entry

    Args: folders - dict of folder dicts ['id': {'id', 'name',...}, ...]
          options - list of menu options for folders

    Returns: False for no entry
             folder - folder object

    """
    num_align = len(str(len(folders)))
    pattern = str("{:>{na}} - {}")
    folder_names = dict(enumerate(folders.values()))
    inp = str("\n").join(pattern.format(j, i['name'], na=num_align)
                         for j, i in folder_names.items())
    sel = dmenu_select(min(bwm.MAX_LEN, len(folders)), prompt, inp=inp)
    if not sel:
        return False
    try:
        return folder_names[int(sel.split(' - ', maxsplit=1)[0])]
    except (ValueError, TypeError):
        return False
Beispiel #21
0
def view_card(entry, folders):
    """Show title, card info and notes for a card entry.

    Returns: dmenu selection

    """
    exp = "Expiration Date: None"
    if entry['card']['expMonth'] or entry['card']['expYear']:
        exp = f"{entry['card']['expMonth']}/{entry['card']['expYear']}"
    fields = [
        entry['name'] or "Title: None",
        obj_name(folders, entry['folderId']), entry['card']['brand']
        or "Card Type: None", entry['card']['cardholderName']
        or "Card Holder Name: None", entry['card']['number']
        or "Card Number: None", exp, entry['card']['code'] or "CVV Code: None",
        "Notes: <Enter to view>" if entry['notes'] else "Notes: None"
    ]
    vault_entries = "\n".join(fields)
    sel = dmenu_select(len(fields), inp=vault_entries)
    if sel == "Notes: <Enter to view>":
        sel = view_notes(entry['notes'])
    elif sel == "Notes: None":
        sel = ""
    return sel
Beispiel #22
0
def select_collection(collections, session,
                      prompt="Collections - Organization (ESC for no selection)",
                      coll_list=False):
    """Select which collection for an entry

    Args: collections - dict of collection dicts {'id': {'id', 'name',...}, ...}
          options - list of menu options for collections
          prompt - displayed prompt
          coll_list - list of collection objects or False if only one collection
                      will be selected

    Returns: collections - dict{id: dict, id1: dict, ...}

    """
    if coll_list is not False:
        # When multiple collections will be selected, they have to come from the
        # same organization.
        org = select_org(session)
        if org is False:
            return False
        orgs = {org['id']: org}
        prompt_name = org['name']
        prompt = f"Collections - {prompt_name} (Enter to select, ESC when done)"
        colls = {i: j for i, j in enumerate(collections.values()) if
                 j['organizationId'] == org['id']}
    else:
        orgs = bwcli.get_orgs(session)
        colls = dict(enumerate(collections.values()))
    num_align = len(str(len(colls)))
    pattern = str("{:>{na}} - {} - {}")

    def check_coll(num, coll_list, cur_coll):
        # Check if name and org_id of cur_coll match in coll_list
        # Return num if not in list, otherwise return "*num"
        for i in coll_list:
            if cur_coll['organizationId'] == i['organizationId'] and cur_coll['name'] == i['name']:
                return f"*{num}"
        return num

    loop = True
    while loop:
        if coll_list is False:
            coll_list = []
            loop = False
        inp = str("\n").join(pattern.format(check_coll(j, coll_list, i),
                                            i['name'],
                                            orgs[i['organizationId']]['name'],
                                            na=num_align)
                             for j, i in colls.items())
        sel = dmenu_select(min(bwm.MAX_LEN, len(colls)), prompt, inp=inp)
        if not sel:
            return {i['id']: i for i in coll_list}
        if sel.startswith('*'):
            sel = sel.lstrip('*')
            try:
                col = colls[int(sel.split(' - ', maxsplit=1)[0])]
                coll_list.remove(col)
            except (ValueError, TypeError):
                loop = False
        else:
            try:
                col = colls[int(sel.split(' - ', maxsplit=1)[0])]
                coll_list.append(col)
            except (ValueError, TypeError):
                loop = False
    return {i['id']: i for i in coll_list}
Beispiel #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
Beispiel #24
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