From bf0461d7d94d3552a7bf1260897832abc544ba74 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Tue, 16 Sep 2025 00:16:41 +0200 Subject: [PATCH 1/3] added create_organisation --- src/vaultwarden/clients/bitwarden.py | 34 +++++++++++++++++++++++++++- src/vaultwarden/utils/crypto.py | 3 +++ tests/e2e/test_bitwarden.py | 5 ++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/vaultwarden/clients/bitwarden.py b/src/vaultwarden/clients/bitwarden.py index 8c8858e..4b5d82b 100644 --- a/src/vaultwarden/clients/bitwarden.py +++ b/src/vaultwarden/clients/bitwarden.py @@ -2,10 +2,11 @@ from uuid import UUID from httpx import Client, Response +from base64 import b64decode, b64encode from vaultwarden.models.exception_models import BitwardenError from vaultwarden.models.sync import ConnectToken, SyncData -from vaultwarden.utils.crypto import make_master_key +from vaultwarden.utils.crypto import make_master_key, encrypt_asym, encrypt_sym, make_asym_key, make_org_key from vaultwarden.utils.logger import log_raise_for_status @@ -132,6 +133,37 @@ def _api_request( method, path, headers=headers, **kwargs ) + def get_public_key_for_user(self, user_id: UUID | None = None) -> str: + resp = self.api_request( + "GET", f"api/users/{self._sync.Profile.Id if not user_id else user_id}/public-key" + ) + return resp.json().get("publicKey") + + + def create_organisation(self, name: str, email: str, default_collection_name: str = "DefaultCollection") -> Response: + encrypted_priv, pub, _ = make_asym_key(self._connect_token.user_key) + + public_key_user = b64decode(self.get_public_key_for_user()) + org_key = make_org_key() + protected_organisation_symetric_key = encrypt_asym(org_key, public_key_user) + + collection = encrypt_sym(bytes(default_collection_name, "utf-8"), org_key) + + payload = { + "key": protected_organisation_symetric_key, + "collectionName": collection, + "name": name, + "billingEmail": email, + "initiationPath": "New organization creation in-product", + "keys": { + "publicKey": b64encode(pub).decode("utf-8"), + "encryptedPrivateKey": encrypted_priv + }, + "planType": 0 + } + resp = self._api_request("POST", "api/organizations", json=payload) + return resp + def sync(self, force_refresh: bool = False) -> SyncData: if self._sync is None or force_refresh: resp = self._api_request("GET", "api/sync") diff --git a/src/vaultwarden/utils/crypto.py b/src/vaultwarden/utils/crypto.py index fa77b8e..092c3c0 100644 --- a/src/vaultwarden/utils/crypto.py +++ b/src/vaultwarden/utils/crypto.py @@ -280,6 +280,9 @@ def make_asym_key(key, stretch=True): private_key = asym_key.exportKey("DER", pkcs=8) return encrypt_sym(private_key, key), public_key, private_key +def make_org_key(): + return token_bytes(64) + def gen_password(length=32, alphabet=None): alphabet = string.ascii_letters + string.digits diff --git a/tests/e2e/test_bitwarden.py b/tests/e2e/test_bitwarden.py index ff59ad5..b1da8d9 100644 --- a/tests/e2e/test_bitwarden.py +++ b/tests/e2e/test_bitwarden.py @@ -123,6 +123,11 @@ def test_add_remove_collection_cipher(self): self.assertEqual(len(res[0].CollectionIds), 2) cipher.update_collection(old_colls) + def test_add_organsiation(self): + res = self.bitwarden.create_organisation("test_me", "me@example.com") + self.assertTrue(res.is_success) + + def test_deduplicate(self): # Todo build test fixtures and delete them at the end of the test return From 9d2e995af46f79738508be53c9db2f728dab36f6 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Tue, 16 Sep 2025 16:30:17 +0200 Subject: [PATCH 2/3] fix: lint --- src/vaultwarden/clients/bitwarden.py | 44 +++++++++++++++++++--------- src/vaultwarden/utils/crypto.py | 1 + tests/e2e/test_bitwarden.py | 2 +- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/vaultwarden/clients/bitwarden.py b/src/vaultwarden/clients/bitwarden.py index 4b5d82b..dad8cfe 100644 --- a/src/vaultwarden/clients/bitwarden.py +++ b/src/vaultwarden/clients/bitwarden.py @@ -1,12 +1,18 @@ +from base64 import b64decode, b64encode from typing import Literal from uuid import UUID from httpx import Client, Response -from base64 import b64decode, b64encode from vaultwarden.models.exception_models import BitwardenError from vaultwarden.models.sync import ConnectToken, SyncData -from vaultwarden.utils.crypto import make_master_key, encrypt_asym, encrypt_sym, make_asym_key, make_org_key +from vaultwarden.utils.crypto import ( + encrypt_asym, + encrypt_sym, + make_asym_key, + make_master_key, + make_org_key, +) from vaultwarden.utils.logger import log_raise_for_status @@ -134,20 +140,30 @@ def _api_request( ) def get_public_key_for_user(self, user_id: UUID | None = None) -> str: - resp = self.api_request( - "GET", f"api/users/{self._sync.Profile.Id if not user_id else user_id}/public-key" - ) + used_id = user_id if user_id else self.sync().Profile.Id + resp = self.api_request("GET", f"api/users/{used_id}/public-key") return resp.json().get("publicKey") - - - def create_organisation(self, name: str, email: str, default_collection_name: str = "DefaultCollection") -> Response: - encrypted_priv, pub, _ = make_asym_key(self._connect_token.user_key) + + + def create_organisation( + self, + name: str, + email: str, + default_collection_name: str = "DefaultCollection", + ) -> Response: + if not self.connect_token: + raise BitwardenError("Not connected") public_key_user = b64decode(self.get_public_key_for_user()) org_key = make_org_key() - protected_organisation_symetric_key = encrypt_asym(org_key, public_key_user) + protected_organisation_symetric_key = encrypt_asym( + org_key, public_key_user + ) - collection = encrypt_sym(bytes(default_collection_name, "utf-8"), org_key) + collection = encrypt_sym( + bytes(default_collection_name, "utf-8"), org_key + ) + encrypted_priv, pub, _ = make_asym_key(self.connect_token.user_key) payload = { "key": protected_organisation_symetric_key, @@ -155,11 +171,11 @@ def create_organisation(self, name: str, email: str, default_collection_name: st "name": name, "billingEmail": email, "initiationPath": "New organization creation in-product", - "keys": { + "keys": { "publicKey": b64encode(pub).decode("utf-8"), - "encryptedPrivateKey": encrypted_priv + "encryptedPrivateKey": encrypted_priv, }, - "planType": 0 + "planType": 0, } resp = self._api_request("POST", "api/organizations", json=payload) return resp diff --git a/src/vaultwarden/utils/crypto.py b/src/vaultwarden/utils/crypto.py index 092c3c0..e4e95ad 100644 --- a/src/vaultwarden/utils/crypto.py +++ b/src/vaultwarden/utils/crypto.py @@ -280,6 +280,7 @@ def make_asym_key(key, stretch=True): private_key = asym_key.exportKey("DER", pkcs=8) return encrypt_sym(private_key, key), public_key, private_key + def make_org_key(): return token_bytes(64) diff --git a/tests/e2e/test_bitwarden.py b/tests/e2e/test_bitwarden.py index b1da8d9..6a312f0 100644 --- a/tests/e2e/test_bitwarden.py +++ b/tests/e2e/test_bitwarden.py @@ -124,7 +124,7 @@ def test_add_remove_collection_cipher(self): cipher.update_collection(old_colls) def test_add_organsiation(self): - res = self.bitwarden.create_organisation("test_me", "me@example.com") + res = bitwarden.create_organisation("test_me", "me@example.com") self.assertTrue(res.is_success) From cd2b02db9cfabb828cbc25e2d1465b6bcec33b54 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Wed, 17 Sep 2025 14:32:22 +0200 Subject: [PATCH 3/3] fix: even more lint --- src/vaultwarden/clients/bitwarden.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vaultwarden/clients/bitwarden.py b/src/vaultwarden/clients/bitwarden.py index dad8cfe..624ee80 100644 --- a/src/vaultwarden/clients/bitwarden.py +++ b/src/vaultwarden/clients/bitwarden.py @@ -143,8 +143,7 @@ def get_public_key_for_user(self, user_id: UUID | None = None) -> str: used_id = user_id if user_id else self.sync().Profile.Id resp = self.api_request("GET", f"api/users/{used_id}/public-key") return resp.json().get("publicKey") - - + def create_organisation( self, name: str,