Secrets manager usage in js edge services (or anywhere wasm doesn't play nice)

Hi, I’m looking to use Bitwarden secrets manager as a secure store programmatically, but running it in lightweight js edge environments where wasm support is finicky (for the examples in this post I’m using cloudflare workers). What I have so far:

  1. You generate an access token with correct permission to project
    • access token is in format 0.<client id uuid>.<client secret>:<base64>
      • I assume the base64 is access token’s unique encryption key (#3)
type UUID = `${string}-${string}-${string}-${string}-${string}`;
interface JWT {
	iss: 'https://identity.bitwarden.com';
	nbf: number;
	iat: number;
	exp: number;
	scode: ['api', 'offline_access'];
	amr: ['Application'];
	client_id: 'web';
	sub: UUID;
	auth_time: number;
	idp: 'bitwarden';
	premium: boolean;
	email: string;
	email_verified: boolean;
	sstamp: UUID;
	name: string;
	orgowner: UUID[];
	accesssecretsmanager: UUID;
	device: UUID;
	jti: string;
}
  1. Make the http requests as below to get to get the secrets (no search unless you list then use language api to search)
interface SecretsProject {
	id: UUID;
	name: `2.${string}|${string}|${string}`;
}
interface Projects {
	object: string;
	projects: SecretsProject[];
	secrets: {
		creationDate: string;
		id: UUID;
		key: `2.${string}|${string}|${string}`;
		organizationId: UUID;
		projects: SecretsProject[];
		read: boolean;
		revisionDate: string;
		write: boolean;
	}[];
}
  1. batch retrieve values
interface Values {
	continuationToken: string;
	data: {
		creationDate: string;
		id: UUID;
		key: string;
		note: string;
		object: string;
		organizationId: UUID;
		projects: SecretsProject[];
		revisionDate: string;
		value: string;
	}[];
	object: 'list';
}
sequenceDiagram
    participant worker
    participant bw as bw-api

    worker->>bw: POST https://identity.bitwarden.com/connect/token with accessToken
    bw-->>worker: Response with access_token
    worker->>bw: GET https://api.bitwarden.com/organizations/:orgId/secrets with jwt
    bw-->>worker: Response with projects and secrets
    worker->>bw: POST https://api.bitwarden.com/secrets/get-by-ids with jwt and secret ids
    bw-->>worker: Return secret values

So where I’m stuck:
The encrypted values all seem to be in the format of 2.<base64>|<base64>|<base64>. The 2nd base64 seems to be the ciphertext as that’s the only one that changes related to the saved secret length. The bws cli does the decryption of these values all client side, these are the only network requests it makes.

So which algorithms and which values do I plug into node:crypto or Web Crypto to be able to decrypt and encrypt secrets back and forth from code to bitwarden?

@demosjarco Welcome to the forum!

Disclaimer: I wrote a response (below) before I realized that you had posted in the Secrets Manager section of the forum, so my answers are all related to the Bitwarden Password Manager. Unfortunately, I don’t know the extent to which the Secrets Manager and the Password Manager use the same encryption algorithms — but if I had to guess, I wouldn’t be surprised if the information in my comment also applies to the Secrets Manager.

 


[Original Response]

 

Basically, the 2. means that AES-CBS-256 encryption with HMAC-SHA256 is being used, and the threee Base-64 components are the initial value, the ciphertext, and the MAC.

The encryption algorithms are described in the documentation, here and here. You may also find Bitwarden’s Interactive Cryptography Page (and its source code) to be useful. In addition, all of the source code for Bitwarden’s clients can be examined on GitHub.

Thanks @grb. For the encryption algorithms, secrets manager and password manager are the same, but dealing with the api responses and what to plug into where and was the main issue. However, I now got it fully working. Summing up the full process below for anyone who stumbles upon this later:

  1. Generate an access token in the web vault with the correct permissions.
    • Access tokens are in format 0.<client_id>.<client_secret>.<16 byte access token encryption key>
  2. Identity exchange:
    • Process is identical as password manager but the scope is api.secrets.
    • access_token is the jwt used for all future api requests
    • encrypted_payload is the organization symmetric key
    sequenceDiagram
    participant worker
    participant bw as bw-api

    worker->>bw: POST https://identity.bitwarden.com/connect/token with accessToken
    Note over worker,bw: Extract `client_id` and `client_secret` from access token
    bw-->>worker: Response with access_token and encrypted_payload
  1. The encrypted_payload is algorithm 2 so we need to derive a 64 byte key from the 16 byte access token key.
    1. We do this by inflating with hkdf iterations until it reaches 32 (sha256 output length)
    2. This key is now used with the same symmetric decryption process and on success, results in a json:
interface decrypted_payload {
	encryptionKey: string;
}
  1. With the jwt as the Bearer auth from step 2, make http requests to the secrets endpoints. A typical flow would include
sequenceDiagram
    participant worker
    participant bw as bw-api

    worker->>bw: GET https://api.bitwarden.com/organizations/:orgId/secrets
    Note over worker,bw: If you don't have the `orgId`, it is in the decoded JWT as `organization`
    bw-->>worker: Response with projects and secrets
    alt Batch api
        worker->>bw: POST https://api.bitwarden.com/secrets/get-by-ids with secret ids
        Note over worker,bw: json body with 1 key (`ids`) and value is secret id array
    else Individual
        worker->>bw: GET https://api.bitwarden.com/secrets/:secretId
    end
    bw-->>worker: Return secret value(s)
  1. Now follow the same standard symmetric decryption process on key, value, and note to get your secret values.

This is the process I followed (and works at the time of writing this post) to use secrets manager programmatically without the cli and it works within any JS edge environment only needing the built in Web Crypto API.

Disclaimer: While this is actually how the bws cli tool actually works behind the scenes, Bitwarden support states:

This is not officially supported. However, there are plans to provide API access in future releases, though I’m not able to provide an ETA for this

Footnote: I have this fully working in a cloudflare workers using only built-in node:buffer, TextDecoder/TextEncoder, and crypto.subtle. While I won’t release it as an npm package, I will link the code that does all of this once I’ve cleaned, commented, and polished it up (right now it is…well let’s just say it works :sweat_smile:)

Due to discourse new user limits, I can only post 2 links per comment so here are the important references:
derive a 64 byte key
hkdf iterations

symmetric decryption process (sdk-internal/crates/bitwarden-crypto/src/enc_string/symmetric.rs at main · bitwarden/sdk-internal · GitHub)