diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..7a5adeb --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,52 @@ +version = 1 + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" + +[[analyzers]] +name = "test-coverage" +enabled = true + +[[analyzers]] +name = "secrets" +enabled = true + +[[transformers]] +name = "prettier" +enabled = true + +[[transformers]] +name = "standardjs" +enabled = true + +[[transformers]] +name = "rubocop" +enabled = true + +[[transformers]] +name = "black" +enabled = true + +[[transformers]] +name = "gofmt" +enabled = true + +[[transformers]] +name = "yapf" +enabled = true + +[[transformers]] +name = "autopep8" +enabled = true + +[[transformers]] +name = "isort" +enabled = true + +[[transformers]] +name = "standardrb" +enabled = true \ No newline at end of file diff --git a/README.md b/README.md index 26a3c18..e2ed96c 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,22 @@ +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/18b28c1dda31468196190062cf9ddb8a)](https://app.codacy.com/gh/Pitenager/passbolt-python-api?utm_source=github.com&utm_medium=referral&utm_content=Pitenager/passbolt-python-api&utm_campaign=Badge_Grade) +[![DeepSource](https://deepsource.io/gh/Pitenager/passbolt-python-api.svg/?label=active+issues&show_trend=true)](https://deepsource.io/gh/Pitenager/passbolt-python-api/?ref=repository-badge) + # Passbolt-python-API ## Installation - $pip install passbolt-python-api + $git clone https://github.com/Pitenager/passbolt-python-api.git + $cd passbolt-python-api.git/ + $pip install passbolt-python-api ## Dependencies - - Python3 - - GPG (also known as GnuPG) software +- Python3 +- GPG (also known as GnuPG) software ## Configuration -Create a config file with the following contents. +Fill the config.ini file with the following contents. [PASSBOLT] SERVER = http:// @@ -21,15 +26,28 @@ Create a config file with the following contents. USER_PRIVATE_KEY_FILE = PASSPHRASE = -## Usage +## CLI usage + + usage: passbolt_manager.py [-h] [-c CHANGE] [-u UPLOAD] [-d DELETE] [-r READ] + + Client to operate Stratio's Passbolt server + + optional arguments: + -h, --help show this help message and exit + -c CHANGE, --change CHANGE Change an existing password in Passbolt + -u UPLOAD, --upload UPLOAD Upload new password to Passbolt + -d DELETE, --delete DELETE Delete an existing password in Passbolt + -r READ, --read READ Read an existing password in Passbolt + +## API Usage >>>import passboltapi >>>passbolt = passboltapi.PassboltAPI(config_path="config.ini") - + # Now you may do any get, post, put and delete request. >>>r = passbolt.get(url="/resources.json?api-version=v2") >>>r = passbolt.post(self.server_url + url, json=data) - + # One can also use it as context manager >>>with passboltapi.PassboltAPI(config_path="config.ini") as passbolt: @@ -42,19 +60,18 @@ To import new keys: >>>import passboltapi >>>passbolt = passboltapi.PassboltAPI(config_path="config.ini", new_keys=True) - + To delete old keys and import only the new ones. >>>import passboltapi >>>passbolt = passboltapi.PassboltAPI(config_path="config.ini", new_keys=True, delete_old_keys=True) -Recommended to do: Do not keep private and public files. +Recommended to do: Do not keep private and public files. Rather just import them using gpg command one time and delete those files. $gpg --import public.asc $gpg --batch --import private.asc - ### Passbolt API For more API related questions, visit Passbolt API documentation: diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..1fc39e1 --- /dev/null +++ b/config.ini @@ -0,0 +1,7 @@ +[PASSBOLT] +SERVER = https:// +SERVER_PUBLIC_KEY_FILE = +USER_FINGERPRINT = +USER_PUBLIC_KEY_FILE = +USER_PRIVATE_KEY_FILE = +PASSPHRASE = diff --git a/passbolt_manager.py b/passbolt_manager.py new file mode 100644 index 0000000..e2de166 --- /dev/null +++ b/passbolt_manager.py @@ -0,0 +1,226 @@ +import argparse +import random +import string +import sys +import time + +from colorama import Fore, init + +import passboltapi + + +def banner(): + print(Fore.CYAN + """ + _ _ _ + _ __ __ _ ___ ___| |__ ___ | | |_ +| '_ \ / _` / __/ __| '_ \ / _ \| | __| +| |_) | (_| \__ \__ \ |_) | (_) | | |_ +| .__/ \__,_|___/___/_.__/ \___/|_|\__|____ +|_| |_____| + _ __ ___ __ _ _ __ __ _ __ _ ___ _ __ +| '_ ` _ \ / _` | '_ \ / _` |/ _` |/ _ \ '__| +| | | | | | (_| | | | | (_| | (_| | __/ | +|_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| + |___/ @Pitenager + """) + + time.sleep(0.5) + + +def login(): + try: + print(Fore.CYAN + + "[*] Trying to authenticate against Passbolt server...") + + passbolt = passboltapi.PassboltAPI(config_path="config.ini", + new_keys=True, + delete_old_keys=True) + uuid = passbolt.get( + url="/resources.json?api-version=v2")["header"]["id"] + + print(Fore.GREEN + "[-] Authenticated") + return passbolt, uuid + + except Exception as e: + print(Fore.RED + "[!] Error: " + str(e)) + sys.exit(1) + + +def generate_password(): + try: + print(Fore.CYAN + "[*] Generating random password...") + + # Create alphanumerical from string constants + printable = f"{string.ascii_letters}{string.digits}{string.punctuation}" + + # Convert printable from string to list and shuffle + printable = list(printable) + random.shuffle(printable) + + # Generate random password and convert to string + random_password = random.choices(printable, k=16) + random_password = "".join(random_password) + + return random_password + + except Exception as e: + print(Fore.RED + "[!] Error: " + str(e)) + sys.exit(1) + + +def read(passbolt, name): + try: + print(Fore.CYAN + f"[*] Reading resource {name}...") + + for i in passbolt.get(url="/resources.json?api-version=v2")["body"]: + if i["name"] == name: + resource = passbolt.get( + "/secrets/resource/{}.json?api-version=v2".format(i["id"])) + i["password"] = passbolt.decrypt(resource["body"]["data"]) + print(i) + break + + except Exception as e: + print(Fore.RED + "[!] Error: " + str(e)) + sys.exit(1) + + +def upload(passbolt, uuid, name): + try: + pwd = generate_password() + encrypted_pass = passbolt.encrypt(pwd) + + json_data = { + "name": name, + "description": f"(Automated) {name} password", + "secrets": [{ + "user_id": uuid, + "data": encrypted_pass + }], + } + + print(Fore.CYAN + "[*] Uploading new password...") + passbolt.post(url="/resources.json?api-version=v2", data=json_data) + print(Fore.GREEN + "[-] Password uploaded") + + except Exception as e: + print(Fore.RED + "[!] Error: " + str(e)) + sys.exit(1) + + +def change(passbolt, name): + try: + pwd = generate_password() + encrypted_pass = passbolt.encrypt(pwd) + + print(Fore.CYAN + f"[*] Changing password of resource {name}...") + + for i in passbolt.get(url="/resources.json?api-version=v2")["body"]: + if i["name"] == name: + json_data = { + "name": + name, + "description": + f"(Automated) {name} password", + "secrets": [{ + "user_id": i["created_by"], + "data": encrypted_pass + }], + } + resourceId = i["id"] + passbolt.put( + url=f"/resources/{resourceId}.json?api-version=v2", + data=json_data) + + print(Fore.GREEN + "[-] Password changed") + + except Exception as e: + print(Fore.RED + "[!] Error: " + str(e)) + sys.exit(1) + + +def delete(passbolt, name): + try: + print(Fore.CYAN + f"[*] Deleting resource {name}...") + for i in passbolt.get(url="/resources.json?api-version=v2")["body"]: + if i["name"] == name: + resourceId = i["id"] + break + passbolt.delete(url=f"/resources/{resourceId}.json?api-version=v2") + print(Fore.GREEN + f"[-] Resource {name} deleted") + except Exception as e: + print(Fore.RED + "[!] Error: " + str(e)) + sys.exit(1) + + +def main(args): + try: + if len(sys.argv) <= 1: + print(Fore.RED + "[!] Error: Should specify at least one argument") + sys.exit(1) + else: + passbolt, uuid = login() + if args.change: + change(passbolt, args.change) + elif args.upload: + upload(passbolt, uuid, args.upload) + elif args.delete: + delete(passbolt, args.delete) + elif args.read: + read(passbolt, args.read) + else: + print(Fore.RED + "[!] Error: Invalid argument") + sys.exit(1) + + print(Fore.CYAN + "[*] Closing session, exiting...") + passbolt.close_session() + print(Fore.GREEN + "[-] Session closed. Finished successfully") + sys.exit(0) + + except Exception as e: + print(Fore.RED + "[!] Error: " + str(e)) + sys.exit(1) + + +if __name__ == "__main__": + init(autoreset=True) + banner() + + # Parameters + parser = argparse.ArgumentParser( + description="Client to operate Stratio's Passbolt server") + parser.add_argument( + "-c", + "--change", + metavar="CHANGE", + dest="change", + default=False, + help="Change an existing password in Passbolt", + ) + parser.add_argument( + "-u", + "--upload", + metavar="UPLOAD", + dest="upload", + default=False, + help="Upload new password to Passbolt", + ) + parser.add_argument( + "-d", + "--delete", + metavar="DELETE", + dest="delete", + default=False, + help="Delete an existing password in Passbolt", + ) + parser.add_argument( + "-r", + "--read", + metavar="READ", + dest="read", + default=False, + help="Read an existing password in Passbolt", + ) + args = parser.parse_args() + + main(args) diff --git a/passboltapi/__init__.py b/passboltapi/__init__.py index ee24990..171fc74 100644 --- a/passboltapi/__init__.py +++ b/passboltapi/__init__.py @@ -1,14 +1,14 @@ -import requests import configparser -import gnupg import urllib.parse +import gnupg +import requests + LOGIN_URL = "/auth/login.json" VERIFY_URL = "/auth/verify.json" class PassboltAPI: - def __init__(self, config_path, new_keys=False, delete_old_keys=False): """ :param config_path: Path to the config file. @@ -30,13 +30,19 @@ def __init__(self, config_path, new_keys=False, delete_old_keys=False): self._import_gpg_keys() try: self.gpg_fingerprint = [ - i for i in self.gpg.list_keys() if i["fingerprint"] == self.config["PASSBOLT"]["USER_FINGERPRINT"] + i for i in self.gpg.list_keys() if i["fingerprint"] == + self.config["PASSBOLT"]["USER_FINGERPRINT"] ][0]["fingerprint"] except IndexError: - raise Exception("GPG public key could not be found. Check: gpg --list-keys") - - if self.config["PASSBOLT"]["USER_FINGERPRINT"] not in [i["fingerprint"] for i in self.gpg.list_keys(True)]: - raise Exception("GPG private key could not be found. Check: gpg --list-secret-keys") + raise Exception( + "GPG public key could not be found. Check: gpg --list-keys") + + if self.config["PASSBOLT"]["USER_FINGERPRINT"] not in [ + i["fingerprint"] for i in self.gpg.list_keys(True) + ]: + raise Exception( + "GPG private key could not be found. Check: gpg --list-secret-keys" + ) self._login() def __enter__(self): @@ -55,42 +61,57 @@ def _delete_old_keys(self): def _import_gpg_keys(self): if not self.config["PASSBOLT"]["USER_PUBLIC_KEY_FILE"]: - raise ValueError("Missing value for USER_PUBLIC_KEY_FILE in config.ini") + raise ValueError( + "Missing value for USER_PUBLIC_KEY_FILE in config.ini") if not self.config["PASSBOLT"]["USER_PRIVATE_KEY_FILE"]: - raise ValueError("Missing value for USER_PRIVATE_KEY_FILE in config.ini") - self.gpg.import_keys(open(self.config["PASSBOLT"]["USER_PUBLIC_KEY_FILE"], "r").read()) - self.gpg.import_keys(open(self.config["PASSBOLT"]["USER_PRIVATE_KEY_FILE"], "r").read()) + raise ValueError( + "Missing value for USER_PRIVATE_KEY_FILE in config.ini") + self.gpg.import_keys( + open(self.config["PASSBOLT"]["USER_PUBLIC_KEY_FILE"], "r").read()) + self.gpg.import_keys( + open(self.config["PASSBOLT"]["USER_PRIVATE_KEY_FILE"], "r").read()) def _login(self): - r = self.requests_session.post(self.server_url + LOGIN_URL, json={ - "gpg_auth": {"keyid": self.gpg_fingerprint}}) + r = self.requests_session.post( + self.server_url + LOGIN_URL, + json={"gpg_auth": { + "keyid": self.gpg_fingerprint + }}, + ) encrypted_token = r.headers["X-GPGAuth-User-Auth-Token"] encrypted_token = urllib.parse.unquote(encrypted_token) encrypted_token = encrypted_token.replace("\+", " ") token = self.decrypt(encrypted_token) - self.requests_session.post(self.server_url + LOGIN_URL, json={ - "gpg_auth": { - "keyid": self.gpg_fingerprint, - "user_token_result": token + self.requests_session.post( + self.server_url + LOGIN_URL, + json={ + "gpg_auth": { + "keyid": self.gpg_fingerprint, + "user_token_result": token + }, }, - }) + ) def encrypt(self, text): - return str(self.gpg.encrypt( - data=text, - recipients=self.gpg_fingerprint, - always_trust=True - )) + return str( + self.gpg.encrypt(data=text, + recipients=self.gpg_fingerprint, + always_trust=True)) def decrypt(self, text): - return str(self.gpg.decrypt( - text, - always_trust=True, - passphrase=str(self.config["PASSBOLT"]["PASSPHRASE"]) - )) + return str( + self.gpg.decrypt( + text, + always_trust=True, + passphrase=str(self.config["PASSBOLT"]["PASSPHRASE"]), + )) def get_headers(self): - return {"X-CSRF-Token": self.requests_session.cookies['csrfToken'] if 'csrfToken' in self.requests_session.cookies else ''} + return { + "X-CSRF-Token": + self.requests_session.cookies["csrfToken"] + if "csrfToken" in self.requests_session.cookies else "" + } def get_server_public_key(self): r = self.requests_session.get(self.server_url + VERIFY_URL) @@ -101,18 +122,21 @@ def get(self, url): return r.json() def post(self, url, data): - r = self.requests_session.post(self.server_url + url, json=data, headers=self.get_headers()) + r = self.requests_session.post(self.server_url + url, + json=data, + headers=self.get_headers()) return r.json() def put(self, url, data): - r = self.requests_session.put(self.server_url + url, json=data, headers=self.get_headers()) + r = self.requests_session.put(self.server_url + url, + json=data, + headers=self.get_headers()) return r.json() def delete(self, url): - r = self.requests_session.delete(self.server_url + url, headers=self.get_headers()) + r = self.requests_session.delete(self.server_url + url, + headers=self.get_headers()) return r.json() def close_session(self): self.requests_session.close() - - diff --git a/setup.py b/setup.py index fc18806..44a55a0 100644 --- a/setup.py +++ b/setup.py @@ -18,19 +18,18 @@ # rm -rf ./dist && python3 setup.py sdist bdist_wheel && python3 -m twine upload dist/* # -from setuptools import setup, find_packages +from setuptools import setup # Usage: python setup.py sdist bdist_wheel links = [] # for repo urls (dependency_links) -with open('requirements.txt') as fp: +with open("requirements.txt") as fp: install_requires = fp.read() DESCRIPTION = "A python client for Passbolt." VERSION = "0.1.2" - with open("README.md", "r") as fh: LONG_DESCRIPTION = fh.read() @@ -41,10 +40,10 @@ author_email="shubham.dipt@gmail.com", description=DESCRIPTION, long_description=LONG_DESCRIPTION, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", url="https://github.com/shubhamdipt/passbolt-python-api", - license=open('LICENSE').read(), - packages=['passboltapi'], + license=open("LICENSE").read(), + packages=["passboltapi"], platforms=["any"], classifiers=( "Programming Language :: Python :: 3", @@ -53,4 +52,4 @@ ), install_requires=install_requires, dependency_links=links, -) \ No newline at end of file +) diff --git a/test.py b/test.py index 8bed6c6..b53fc0a 100644 --- a/test.py +++ b/test.py @@ -2,13 +2,13 @@ def get_my_passwords(passbolt_obj): - result = list() + result = [] for i in passbolt_obj.get(url="/resources.json?api-version=v2")["body"]: result.append({ "id": i["id"], "name": i["name"], "username": i["username"], - "uri": i["uri"] + "uri": i["uri"], }) print(i) for i in result: @@ -30,5 +30,5 @@ def main(): get_my_passwords(passbolt_obj=passbolt) -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main()