From c87f6d6e72e45ec6ff82523b4a9c7dbbe80c377a Mon Sep 17 00:00:00 2001 From: Felix Deimel Date: Fri, 8 Nov 2024 13:41:41 +0100 Subject: [PATCH] [Bitwarden] Add API key based authentication support --- .../Bitwarden/Bitwarden (Python).rdfe | 42 -- .../Bitwarden/Bitwarden (Python).rdfx | 486 ++++++++++++++++++ 2 files changed, 486 insertions(+), 42 deletions(-) delete mode 100644 Dynamic Folder/Bitwarden/Bitwarden (Python).rdfe create mode 100644 Dynamic Folder/Bitwarden/Bitwarden (Python).rdfx diff --git a/Dynamic Folder/Bitwarden/Bitwarden (Python).rdfe b/Dynamic Folder/Bitwarden/Bitwarden (Python).rdfe deleted file mode 100644 index da77d32..0000000 --- a/Dynamic Folder/Bitwarden/Bitwarden (Python).rdfe +++ /dev/null @@ -1,42 +0,0 @@ -{ - "Name": "Dynamic Folder Export", - "Objects": [ - { - "Type": "DynamicFolder", - "Name": "Bitwarden (Python)", - "Description": "This Dynamic Folder sample allows you to import credentials from Bitwarden.", - "Notes": "

Bitwarden Dynamic Folder sample

\n\n

Version: 1.0.6
\nAuthor: Royal Apps

\n\n

This Dynamic Folder sample allows you to import credentials from Bitwarden. The Bitwarden CLI client is required and the path where it is installed must be configured in the "Custom Properties" section. Also, your Bitwarden login details must be provided in the "Credentials" section.

\n\n

At the moment, all items are placed in the root folder. There is no support for custom folders at the moment. Bitwarden two-step login is supported, but only tested with the "Authenticator App" and "Email" providers, the 2FA (TOTP) code can be automatically generated.

\n\n

Requirements

\n\n\n\n

Setup

\n\n\n", - "CustomProperties": [ - { - "Name": "Bitwarden CLI Configuration", - "Type": "Header", - "Value": "" - }, - { - "Name": "BW Path (Windows)", - "Type": "Text", - "Value": "C:\\Program Files\\bw.exe" - }, - { - "Name": "BW Path (macOS)", - "Type": "Text", - "Value": "/Applications/bw" - }, - { - "Name": "Two-step Login Configuration", - "Type": "Header", - "Value": "" - }, - { - "Name": "TOTP key", - "Type": "Protected", - "Value": "" - } - ], - "Script": "from __future__ import print_function\r\nfrom sys import platform as _platform\r\nfrom functools import partial\r\n\r\nimport sys\r\nimport json\r\nimport subprocess\r\nimport os\r\n\r\nis_macOS = _platform.lower().startswith(\"darwin\")\r\nhave_pyotp = False\r\n\r\ntry:\r\n import pyotp\r\n have_pyotp = True\r\nexcept:\r\n pass\r\n\r\ntry:\r\n\t# for Python2\r\n\tfrom Tkinter import * \r\nexcept ImportError:\r\n\t# for Python3\r\n\tfrom tkinter import *\r\n\r\nclass TakeInput(object):\r\n\tdef __init__(self, request_message):\r\n\t\tself.root = Tk()\r\n\r\n\t\ttitle = \"\"\r\n\r\n\t\tif request_message:\r\n\t\t\ttitle = request_message\r\n\t\t\t\r\n\t\t\tif title.endswith(\":\"):\r\n\t\t\t\ttitle = title[:-1]\r\n\r\n\t\tself.root.title(title)\r\n\r\n\t\t# Do not allow the user to resize the window\r\n\t\tself.root.resizable(False, False)\r\n\t\t\r\n\t\tself.string = \"\"\r\n\r\n\t\tself.frame = Frame(self.root)\r\n\r\n\t\tself.acceptInput(request_message)\r\n\t\tself.frame.pack(padx=17, pady=17)\r\n\r\n\tdef acceptInput(self, request_message):\r\n\t\tr = self.frame\r\n\r\n\t\ticon = Label(r, text=\"\", image=\"::tk::icons::question\")\r\n\t\ticon.grid(row=0, column=0, rowspan=2, sticky=\"w\")\r\n\r\n\t\tlabel = Label(r, text=request_message)\r\n\t\tlabel.grid(row=0, column=1, padx=(9, 0), sticky=\"nw\")\r\n\t\t\r\n\t\tself.e = Entry(r, text='Name')\r\n\t\tself.e.grid(row=1, column=1, padx=(13, 0), sticky=\"nw\")\r\n\t\tself.e.configure(width=30)\r\n\t\tself.e.focus_set()\r\n\r\n\t\tb = Button(r, text=' OK ', command=self.gettext)\r\n\t\tb.grid(row=2, column=1, sticky=\"ne\", pady=(10, 0))\r\n\t\t\r\n\t\tself.root.bind('', self.gettext)\r\n\r\n\tdef gettext(self, event=None):\r\n\t\tself.string = self.e.get()\r\n\t\tself.root.destroy()\r\n\r\n\tdef getString(self):\r\n\t\treturn self.string\r\n\t\r\n\tdef configureWindowGeometry(self):\r\n\t\t# Get the window size\r\n\t\twindow_width = self.root.winfo_width()\r\n\t\twindow_height = self.root.winfo_height()\r\n\r\n\t\t# Get the screen size\r\n\t\tscreen_width = self.root.winfo_screenwidth()\r\n\t\tscreen_height = self.root.winfo_screenheight()\r\n\r\n\t\t# Get the window position from the top dynamically as well as position from left or right as follows\r\n\t\tposition_top = int((screen_height / 2) - (window_height / 2))\r\n\t\tposition_right = int((screen_width / 2) - (window_width / 2))\r\n\r\n\t\t# Shift up by a couple of pixels to account for the title bar\r\n\t\tposition_top -= 30\r\n\r\n\t\t# This will center the window\r\n\t\tself.root.geometry(str(window_width) + \"x\" + str(window_height) + \"+\" + str(position_right) + \"+\" + str(position_top))\r\n\r\n\tdef waitForInput(self):\r\n\t\tself.root.lift()\r\n\r\n\t\tself.root.attributes('-topmost', True)\r\n\t\tself.root.after_idle(self.root.attributes, '-topmost', False)\r\n\r\n\t\t# Ensure that layout is ready\r\n\t\tself.root.update_idletasks()\r\n\r\n\t\tself.configureWindowGeometry()\r\n\r\n\t\tself.root.mainloop()\r\n\r\ndef show_prompt(request_message):\r\n\tmsg_box = TakeInput(request_message)\r\n\r\n\t# loop until the user makes a decision and the window is destroyed\r\n\tmsg_box.waitForInput()\r\n\r\n\treturn msg_box.getString()\r\n\r\n\r\ndef convert_notes_to_html(notes):\r\n\tif notes is None:\r\n\t\treturn \"\"\r\n\telse:\r\n\t\treturn notes.replace(\"\\r\\n\", \"
\").replace(\"\\r\", \"
\").replace(\"\\n\", \"
\")\r\n\r\n\r\ndef create_credential(item):\r\n\titem_id = item[\"id\"]\r\n\titem_type = item[\"type\"]\r\n\titem_name = item[\"name\"]\r\n\titem_notes = convert_notes_to_html(item.get(\"notes\", \"\"))\r\n\titem_favorite = item.get(\"favorite\", False)\r\n\r\n\titem_login = item.get(\"login\", None)\r\n\r\n\titem_username = \"\"\r\n\titem_password = \"\"\r\n\titem_url = \"\"\r\n\r\n\tif item_login is not None:\r\n\t\titem_username = item_login.get(\"username\", \"\")\r\n\t\titem_password = item_login.get(\"password\", \"\")\r\n\r\n\t\titem_uris = item_login.get(\"uris\", None)\r\n\r\n\t\tif item_uris is not None:\r\n\t\t\tfor item_uri in item_login.get(\"uris\", None):\r\n\t\t\t\titem_url = item_uri.get(\"uri\", \"\")\r\n\t\r\n\titem_fields = item.get(\"fields\", None)\r\n\r\n\titem_custom_properties = [ ]\r\n\r\n\tif item_type == 3: # Card\r\n\t\titem_card = item.get(\"card\", None)\r\n\r\n\t\tif item_card is not None:\r\n\t\t\tcard_brand = item_card.get(\"brand\", \"Credit Card\")\r\n\t\t\tcard_cardholdername = item_card.get(\"cardholderName\", None)\r\n\t\t\tcard_code = item_card.get(\"code\", None)\r\n\t\t\tcard_expiration_month = item_card.get(\"expMonth\", None)\r\n\t\t\tcard_expiration_year = item_card.get(\"expYear\", None)\r\n\t\t\tcard_number = item_card.get(\"number\", None)\r\n\r\n\t\t\titem_custom_properties.append({\r\n\t\t\t\t\"Type\": \"Header\",\r\n\t\t\t\t\"Name\": card_brand\r\n\t\t\t})\r\n\r\n\t\t\tif card_cardholdername is not None:\r\n\t\t\t\titem_custom_properties.append({\r\n\t\t\t\t\t\"Type\": \"Text\",\r\n\t\t\t\t\t\"Name\": \"Cardholder\",\r\n\t\t\t\t\t\"Value\": card_cardholdername\r\n\t\t\t\t})\r\n\r\n\t\t\tif card_number is not None:\r\n\t\t\t\titem_custom_properties.append({\r\n\t\t\t\t\t\"Type\": \"Text\",\r\n\t\t\t\t\t\"Name\": \"Card Number\",\r\n\t\t\t\t\t\"Value\": card_number\r\n\t\t\t\t})\r\n\r\n\t\t\tif card_expiration_month is not None:\r\n\t\t\t\titem_custom_properties.append({\r\n\t\t\t\t\t\"Type\": \"Text\",\r\n\t\t\t\t\t\"Name\": \"Expiration Month\",\r\n\t\t\t\t\t\"Value\": card_expiration_month\r\n\t\t\t\t})\r\n\r\n\t\t\tif card_expiration_year is not None:\r\n\t\t\t\titem_custom_properties.append({\r\n\t\t\t\t\t\"Type\": \"Text\",\r\n\t\t\t\t\t\"Name\": \"Expiration Year\",\r\n\t\t\t\t\t\"Value\": card_expiration_year\r\n\t\t\t\t})\r\n\r\n\t\t\tif card_code is not None:\r\n\t\t\t\titem_custom_properties.append({\r\n\t\t\t\t\t\"Type\": \"Protected\",\r\n\t\t\t\t\t\"Name\": \"Security Code\",\r\n\t\t\t\t\t\"Value\": card_code\r\n\t\t\t\t})\r\n\r\n\r\n\tif item_fields is not None:\r\n\t\tfor item_field in item_fields:\r\n\t\t\titem_field_type = item_field[\"type\"]\r\n\t\t\titem_field_name = item_field.get(\"name\", \"\")\r\n\t\t\titem_field_value = item_field.get(\"value\", \"\")\r\n\r\n\t\t\tcustom_property_type = \"Text\"\r\n\r\n\t\t\tif item_field_type == 1:\r\n\t\t\t\tcustom_property_type = \"Protected\"\r\n\t\t\telif item_field_type == 2:\r\n\t\t\t\tcustom_property_type = \"YesNo\"\r\n\t\t\t\titem_field_value = bool(item_field_value)\r\n\t\t\t\r\n\t\t\tif item_field_name is None:\r\n\t\t\t\titem_field_name = \"\"\r\n\t\t\t\t\r\n\t\t\tif item_field_value is None:\r\n\t\t\t\titem_field_value = \"\"\r\n\t\t\t\r\n\t\t\tcustom_property = {\r\n\t\t\t\t\"Type\": custom_property_type,\r\n\t\t\t\t\"Name\": item_field_name,\r\n\t\t\t\t\"Value\": item_field_value\r\n\t\t\t}\r\n\r\n\t\t\titem_custom_properties.append(custom_property)\r\n\r\n\r\n\tcredential = {\r\n\t\t\"Type\": \"Credential\",\r\n\t\t\"ID\": item_id,\r\n\t\t\"Name\": item_name,\r\n\t\t\"Notes\": item_notes,\r\n\t\t\"Favorite\": item_favorite,\r\n\t\t\"Username\": item_username,\r\n\t\t\"Password\": item_password,\r\n\t\t\"URL\": item_url,\r\n\t\t\"CustomProperties\": item_custom_properties\r\n\t}\r\n\t\r\n\treturn credential\r\n\r\n\r\ndef logout(bw_path):\r\n\tcmd_logout = [ bw_path, \"logout\" ]\r\n\tbw = subprocess.Popen(cmd_logout, stdout=subprocess.PIPE)\r\n\tbw.wait()\r\n\r\n\r\ndef get_entries(bw_path, username, password, totp_key):\r\n\tprintError = partial(print, file=sys.stderr) # python2 compatibility\r\n\r\n\tif not username or not password:\r\n\t\tprintError(\"Login failed. Please specify your username and password.\")\r\n\r\n\t\tsys.exit(1)\r\n\r\n\tlogout(bw_path)\r\n\r\n\tcmd_login = [ bw_path, \"login\", username, password, \"--raw\" ]\r\n\t\r\n\tbw = subprocess.Popen(cmd_login, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE)\r\n\r\n\tout_buffer = \"\"\r\n\r\n\tsend_two_step_code = False\r\n\tmultiple_two_step_methods = False\r\n\r\n\twhile True:\r\n\t\tout = bw.stdout.read(1).decode(\"utf-8\")\r\n\r\n\t\tout_buffer += out\r\n\r\n\t\tif not send_two_step_code:\r\n\t\t\tif out_buffer == '? Two-step login code:':\r\n\t\t\t\tsend_two_step_code = True\r\n\r\n\t\t\t\tsys.stdout.flush()\r\n\r\n\t\t\t\tif have_pyotp and totp_key:\r\n\t\t\t\t\tmfa_code = pyotp.TOTP(totp_key).now()\r\n\t\t\t\telse:\r\n\t\t\t\t\tmfa_code = show_prompt(\"Please enter your two-step login code:\")\r\n\r\n\t\t\t\tbw.stdin.write((mfa_code + \"\\n\").encode(\"utf-8\"))\r\n\r\n\t\t\t\tbreak\r\n\t\t\telif out_buffer == '? Two-step login method:':\r\n\t\t\t\tmultiple_two_step_methods = True\r\n\r\n\t\t\t\tsys.stdout.flush()\r\n\r\n\t\t\t\tbreak\r\n\t\t\r\n\t\tif out == \"\" and bw.poll() is not None:\r\n\t\t\tbreak\r\n\r\n\tif multiple_two_step_methods:\r\n\t\tprintError(\"Login failed. Multiple two-step login methods are enabled. This script only supports a single two-step login method.\")\r\n\r\n\t\tsys.exit(1)\r\n\r\n\t(session_key, err) = bw.communicate()\r\n\r\n\tif send_two_step_code:\r\n\t\tsession_key = session_key.decode(\"utf-8\")\r\n\telse:\r\n\t\tsession_key = out_buffer\r\n\r\n\tout_buffer_split = session_key.split(\"\\n\")\r\n\r\n\tif out_buffer_split is not None and len(out_buffer_split) >= 1:\r\n\t\tout_buffer_split = list(filter(None, out_buffer_split))\r\n\r\n\t\tlast_line = out_buffer_split[len(out_buffer_split) - 1]\r\n\r\n\t\tsession_key = last_line\r\n\r\n\texit_code = bw.wait()\r\n\r\n\tif exit_code != 0:\r\n\t\tprintError(\"Login failed, please verify your credentials.\")\r\n\r\n\t\tsys.exit(1)\r\n\r\n\tcmd_sync = [ bw_path, \"sync\", \"--session\", session_key ]\r\n\r\n\tbw = subprocess.Popen(cmd_sync, stdout=subprocess.PIPE)\r\n\tbw.wait()\r\n\r\n\tcmd_list_items = [ bw_path, \"list\", \"items\", \"--session\", session_key ]\r\n\r\n\tbw = subprocess.Popen(cmd_list_items, stdout=subprocess.PIPE)\r\n\t(list_items_json, err) = bw.communicate()\r\n\texit_code = bw.wait()\r\n\r\n\tif exit_code != 0:\r\n\t\tprintError(\"Listing items failed.\")\r\n\r\n\t\tsys.exit(1)\r\n\r\n\tlist_items_response = json.loads(list_items_json)\r\n\r\n\tstore_objects = []\r\n\r\n\tfor item in list_items_response:\r\n\t\tcred = create_credential(item)\r\n\r\n\t\tstore_objects.append(cred)\r\n\r\n\tstore = {\r\n\t\t\"Objects\": store_objects\r\n\t}\r\n\t\r\n\tstore_json = json.dumps(store)\r\n\t\r\n\treturn store_json\r\n\r\nbw_path_windows = r\"$CustomProperty.BWPathWindows$\"\r\nbw_path_macOS = r\"$CustomProperty.BWPathmacOS$\"\r\n\r\nbw_path = bw_path_macOS if is_macOS else bw_path_windows\r\nbw_path = os.path.expandvars(bw_path)\r\n\r\nprint(get_entries(bw_path, r\"$EffectiveUsername$\", r\"$EffectivePassword$\", r\"$CustomProperty.TOTPkey$\"))\r\nlogout(bw_path)", - "ScriptInterpreter": "python", - "DynamicCredentialScriptInterpreter": "json", - "DynamicCredentialScript": "{\n\t\"Username\": \"user\",\n\t\"Password\": \"pass\"\n}" - } - ] -} \ No newline at end of file diff --git a/Dynamic Folder/Bitwarden/Bitwarden (Python).rdfx b/Dynamic Folder/Bitwarden/Bitwarden (Python).rdfx new file mode 100644 index 0000000..aedea25 --- /dev/null +++ b/Dynamic Folder/Bitwarden/Bitwarden (Python).rdfx @@ -0,0 +1,486 @@ + + Dynamic Folder Export + + + DynamicFolder + Bitwarden (Python) + This Dynamic Folder sample allows you to import credentials from Bitwarden. + Bitwarden Dynamic Folder sample + +

Version: 1.0.7
+Author: Royal Apps

+ +

This Dynamic Folder sample allows you to import credentials from Bitwarden. The Bitwarden CLI client is required and the path where it is installed must be configured in the "Custom Properties" section. Also, your Bitwarden login details must be provided in the "Credentials" section. Optionally, you can configure API Key based authentication by providing your Client ID and Client Secret in the "Custom Properties" section. Note that, your Bitwarden password must still be provided even when using API Key based authentication.

+ +

At the moment, all items are placed in the root folder. There is no support for custom folders at the moment. Bitwarden two-step login is supported, but only tested with the "Authenticator App" and "Email" providers, the 2FA (TOTP) code can be automatically generated.

+ +

Note that if your account is hosted on any other domain than vault.bitwarden.com, you must configure that domain using the Bitwarden command-line tool (CLI) before using this dynamic folder script. (see https://bitwarden.com/help/cli/#config).

+ +

Requirements

+ +
    +
  • Bitwarden command-line tool (CLI)
  • +
  • Python Module: __future__
  • +
  • Python Module: sys
  • +
  • Python Module: functools
  • +
  • Python Module: json
  • +
  • Python Module: subprocess
  • +
  • Python Module: os
  • +
  • Python Module: tkinter
  • +
  • Python Module: pyotp (only needed when using the auto TOTP 2FA code generation)
  • +
+ +

Setup

+ +
    +
  • Specify the full, absolute path to the Bitwarden CLI tool in the "Custom Properties" section.
  • +
  • Specify credentials for accessing your Bitwarden vault in the "Credentials" section.
  • +
  • When using the auto TOTP 2FA code generation, specify the TOTP key in the "Custom Properties" section.
  • +
  • Optionally, specify your Client ID and Client Secret for API Key based authentication in the "Custom Properties" section.
  • +
+]]>
+ + + Bitwarden CLI Configuration + Header + + + + BW Path (Windows) + Text + C:\Program Files\bw.exe + + + BW Path (macOS) + Text + /Applications/bw + + + Two-step Login Configuration + Header + + + + TOTP key + Protected + + + + API Key Authentication + Header + + + + Client ID + Text + + + + Client Secret + Protected + + + + python + + json + + ReplaceInline + DynFolder_ + ReplaceInline + DynCredential_ +
+
+
\ No newline at end of file