Écrire un client Bitwarden en python : créer une organisation et une collection

Je suis en train d'écrire un logiciel qui va interagir avec Bitwarden. Il existe plusieurs clients Bitwarden, mais peu sont suffisamment complet pour mon besoin.

Ce billet fait suite au premier billet écrire un client Bitwarden en python : créer, connecter, valider un compte et au deuxième billet écrire un client Bitwarden en python : créer un identifiant dans mon coffre

On va voir comment créer une organisation et une collection.

Une organisation est un espace de stockage où un ensemble d'utilisateur peut partager des éléments.

Une collection (à ne pas confondre avec les répertoires) permet de ranger les éléments et de les partager avec les membres d'une organisation.

Créer une organisation

Avant de créer une organisation, il va falloir créer une clé symétrique. On ne peut pas utiliser la clé symétrique de son coffre, car l'objectif des organisations est de pouvoir partage des informations avec d'autres utilisateurs. Cette clé symétrique va donc être partage avec tous les utilisateurs.

Ne pouvant être stocké en clair dans la base de Bitwarden et ne pouvant accéder aux clés symétriques des autres utilisateurs, cette clé va être chiffrée avec les clés publiques des clés asymétriques de tous les utilisateurs accédant à cette organisation.

Les utilisateurs utiliseront leur clé privée, protégé, pour accéder à la clé symétrique.

C'est pour cela qu'à la création des utilisateurs nous avons créé une clé asymétrique.

Récupérons la clé asymétrique (la variable login ci-dessous provient de la connexion de l'utilisateur) :

private_key = login['PrivateKey']

Cette clé est chiffrée avec la clé symétrique de l'utilisateur. Il faut donc commencer par la déchiffrer comme vu dans le précédent billet.

On génère ensuite une clé symétrique pour cette organisation :

token = token_bytes(64)

Et on la chiffre avec la clé asymétrique précédemment déchiffrée :

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

rsa_key = RSA.importKey(private_key)
cipher = PKCS1_OAEP.new(rsa_key).encrypt(token)
b64_cipher = b64encode(cipher).decode()

Le secret est mis dans une chaine avec le type 4 pour préciser que la donnée est chiffrée par une clé asymétrique :

encoded_key = f"4.{b64_cipher}"

Pour créer l'organisation il nous manque :

  1. une adresse courriel de facturation (je suppose que c'est utile uniquement sur la plateforme officielle)
  2. le nom de l'organisation chiffrée avec la clé symétrique de cette organisation
  3. le nom de la collection par défaut
data = {"key": encoded_key,
            "collectionName": encoded_organization_name,
            "name": collection_name,
            "billingEmail": email,
            "planType": 0,
        }

Il faut faire un POST avec ce payload sur l'URL ''https://localhost:8001/api/organizations' pour créer l'organisation. Dans l'entête il faut rajouter les informations d'authentification :

{'Authorization': f'Bearer {login["access_token"]}'}

Créer une collection

La création d'une collection est assez simple.

Il faut juste chiffrer avec la clé symétrique de la collection le nom de la collection.

 data = {"groups": [],
               "name": encrypted_name
              }

L'URL pour créer la collection est construite à partir de l'ID de l'organisation : f'https://localhost:8001/api/organizations/{organization_id}/collections'.

Il faut faire un POST avec ce payload sur cette URL. Dans l'entête il faut rajouter les informations d'authentification :

{'Authorization': f'Bearer {login["access_token"]}'}

Invitée un autre utilisateur à cette organisation

L'invitation de l'utilisateur ne pose par de problème (ici je ne m'occupe pas de la gestion des droits) :

email = 'other@gnunux.info'
data = {'emails': [email],
             'collections': None,
             'accessAll': True,
             'type': 2,
             }

L'URL pour invité un utilisateur à la collection est construite à partir de l'ID de l'organisation : f'https://localhost:8001/api/organizations/{organization_id}/users/invite'.

Il faut faire un POST avec ce payload sur cette URL. Dans l'entête il faut rajouter les informations d'authentification :

{'Authorization': f'Bearer {login["access_token"]}'}

Confirmer l'invitation

L'utilisateur devrait alors accepter cette invitation. Un courriel est envoyé à l'utilisateur qui devra cliquer sur le lien et s'authentifier sur Bitwarden.

Il est possible d'automatiser ce processus comme lors de la création d'un utilisateur mais cette fonctionnalité ne m'intéresse pas.

Une fois que l'utilisateur a accepté l'invitation, il faut confirmer l'invitation.

La confirmation consiste principalement à chiffrer la clé symétrique de cette organisation avec la clé publique de la clé asymétrique de l'utilisateur.

Le plus simple pour récupérer cette clé publique, et de récupérer les informations de l'organisation.

Il suffit pour cela de faire un GET sur l'URL f'https://localhost:8001/api/organizations/{organization_id}/users' et de récupérer la réponse dans la variable "users". Dans l'entête il faut rajouter les informations d'authentification :

{'Authorization': f'Bearer {login["access_token"]}'}

À partir de ces informations on va rechercher le numéro utilisateur :

> for user in users['Data']:
>     if user['Email'] == email:
>         user_id = user["UserId"]
>         break

Et nous allons récupérer la clé publique.

Il suffit pour cela de faire un GET sur l'URL f'https://localhost:8001/api/users/{user_id}/public-key' et de récupérer la réponse dans la variable "user_public_key". Dans l'entête il faut rajouter les informations d'authentification :

{'Authorization': f'Bearer {login["access_token"]}'}

Et on retrouve la clé publique :

public_key = b64decode(user_public_key['PublicKey'])

Et on chiffre la clé symétrique de l'organisation avec cette clé publique de la même façon qu'on a chiffrée au-dessus pour la sienne.

Enfin on construit le payload :

data = {"key": key}

L'URL pour confirmer l'invitation d'un utilisateur à la collection est construite à partir de l'ID de l'organisation et de l'utilisateur : f'https://localhost:8001/'api/organizations/{organization_id}/users/{user_id}/confirm'.

Il faut faire un POST avec ce payload sur cette URL. Dans l'entête il faut rajouter les informations d'authentification :

{'Authorization': f'Bearer {login["access_token"]}'}

Pour finir ...

Avec les informations contenues dans ces trois billets, vous devriez être capable d'intéragir avec Bitwarden.

Pour comprendre le fonctionnement de Bitwarden je me suis servi :

  1. de la documentation de l'API de Bitwarden
  2. l'implémentation rust de Bitwarden
  3. de l'implémentation officielle du client en typescript/javascript
  4. d'une implémentation d'un client en python
  5. d'une implémentation d'un client en ruby
  6. d'une implémentation d'un client en rust

Haut de page