From 83229b45e273ee659cd0e300650e549b66ed0aee Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 17 Oct 2023 22:20:39 +0200 Subject: [PATCH] Add Bitwarden client --- scripts/.gitignore | 1 + .../p7s/__pycache__/__init__.cpython-39.pyc | Bin 138 -> 0 bytes scripts/p7s/appdirs.py | 11 ++ scripts/p7s/bitwarden.py | 61 ++++++++ scripts/poetry.lock | 146 +++++++++++++++++- scripts/pyproject.toml | 2 + 6 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 scripts/.gitignore delete mode 100644 scripts/p7s/__pycache__/__init__.cpython-39.pyc create mode 100644 scripts/p7s/appdirs.py create mode 100644 scripts/p7s/bitwarden.py diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/scripts/p7s/__pycache__/__init__.cpython-39.pyc b/scripts/p7s/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 483c7b9cf902e6974a89a2345e05678ce3f19946..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmYe~<>g`kf;Z>&QbF`%5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!H%enx(7s(xZl zYK4A!W(k;4kWyf-Uz}W&Sx{1}UtnIWA0MBYmst`YuUAlci^C>2KczG$)edCPXCP(( E0Hw7b4FCWD diff --git a/scripts/p7s/appdirs.py b/scripts/p7s/appdirs.py new file mode 100644 index 0000000..9ff0ca8 --- /dev/null +++ b/scripts/p7s/appdirs.py @@ -0,0 +1,11 @@ +import appdirs +import pathlib + + +APPDIRS = appdirs.AppDirs("p7s", "alex@pdp7.net") + + +def user_cache_dir(): + r = pathlib.Path(APPDIRS.user_cache_dir) + r.mkdir(parents=True, exist_ok=True) + return r diff --git a/scripts/p7s/bitwarden.py b/scripts/p7s/bitwarden.py new file mode 100644 index 0000000..9ce4d44 --- /dev/null +++ b/scripts/p7s/bitwarden.py @@ -0,0 +1,61 @@ +import contextlib +import io +import json +import os +import subprocess +import zipfile + +import httpx + +from p7s import appdirs + + +class Bitwarden(): + def download(self, check_version=False): + self.bw_command = appdirs.user_cache_dir() / "bw" + if self.bw_command.exists() and not check_version: + return self.bw_command + r = httpx.get("https://vault.bitwarden.com/download/?app=cli&platform=linux") + location = r.headers["location"] + version = location.split("/")[7] + bw_versioned_command = appdirs.user_cache_dir() / f"bw-{version}" + if not bw_versioned_command.exists(): + with zipfile.ZipFile(io.BytesIO(httpx.get(location, follow_redirects=True).content)) as zip: + with zip.open("bw") as zip_bw: + bw_versioned_command.write_bytes(zip_bw.read()) + bw_versioned_command.chmod(0o755) + self.bw_command.unlink(missing_ok=True) + self.bw_command.symlink_to(bw_versioned_command) + + @contextlib.contextmanager + def login(self, server, email): + subprocess.run([self.bw_command, "config", "server", server], check=True) + status = self.status()["status"] + if status == "unauthenticated": + command = ["login", email] + elif status == "locked": + command = ["unlock"] + else: + assert False, f"unexpected status {status}" + command = subprocess.run([self.bw_command] + command, check=True, stdout=subprocess.PIPE, encoding="UTF8") + export_line = command.stdout.splitlines()[3] + session = export_line.split('"')[1] + os.environ["BW_SESSION"] = session + try: + yield self.bw_command + subprocess.run([self.bw_command, "lock"], check=True) + finally: + del os.environ["BW_SESSION"] + + def status(self): + return json.loads(subprocess.run([self.bw_command, "status"], check=True, stdout=subprocess.PIPE).stdout) + + def get_item(self, uuid): + return json.loads(subprocess.run([self.bw_command, "get", "item", uuid], check=True, stdout=subprocess.PIPE).stdout) + + +def get_item(server, email, uuid): + b = Bitwarden() + b.download() + with b.login(server, email): + return b.get_item(uuid) diff --git a/scripts/poetry.lock b/scripts/poetry.lock index 9c20220..fec9836 100644 --- a/scripts/poetry.lock +++ b/scripts/poetry.lock @@ -1,7 +1,149 @@ # This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. -package = [] + +[[package]] +name = "anyio" +version = "4.0.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, + {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.22)"] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "0.18.0" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"}, + {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.25.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, + {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.18.0,<0.19.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "c595a0588c25d58f3e3834ad7169126836d262b925fe6ca9b5d540dcf301d254" +content-hash = "a6328570e23a2739e1268265645d92a5d4cfb77f85586e54e9cdf4a161fd690d" diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index ad8aef7..8e80de4 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -6,6 +6,8 @@ authors = ["alex "] [tool.poetry.dependencies] python = "^3.9" +appdirs = "^1.4.4" +httpx = "^0.25.0" [build-system] -- 2.47.3