1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-27 14:53:44 +00:00

Add unlock service

This commit is contained in:
Bernd Schoolmann
2026-01-20 17:48:13 +01:00
parent cae3a0587f
commit babe20af06
19 changed files with 358 additions and 2 deletions

1
.github/CODEOWNERS vendored
View File

@@ -234,3 +234,4 @@ libs/pricing @bitwarden/team-billing-dev
.github/workflows/respond.yml @bitwarden/team-ai-sme
.github/workflows/review-code.yml @bitwarden/team-ai-sme
libs/subscription @bitwarden/team-billing-dev
libs/unlock @bitwarden/team-key-management-dev

View File

@@ -61,6 +61,7 @@ module.exports = {
"<rootDir>/libs/vault/jest.config.js",
"<rootDir>/libs/auto-confirm/jest.config.js",
"<rootDir>/libs/subscription/jest.config.js",
"<rootDir>/libs/unlock/jest.config.js",
],
// Workaround for a memory leak that crashes tests in CI:

View File

@@ -35,7 +35,7 @@ export class MasterPasswordUnlockData {
readonly salt: MasterPasswordSalt,
readonly kdf: KdfConfig,
readonly masterKeyWrappedUserKey: MasterKeyWrappedUserKey,
) {}
) { }
static fromSdk(sdkData: SdkMasterPasswordUnlockData): MasterPasswordUnlockData {
return new MasterPasswordUnlockData(
@@ -45,6 +45,14 @@ export class MasterPasswordUnlockData {
);
}
toSdk(): SdkMasterPasswordUnlockData {
return {
salt: this.salt,
kdf: this.kdf.toSdkConfig(),
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey,
};
}
toJSON(): any {
return {
salt: this.salt,

5
libs/unlock/README.md Normal file
View File

@@ -0,0 +1,5 @@
# unlock
Owned by: key-management
Unlock the account of a user

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

View File

@@ -0,0 +1,10 @@
module.exports = {
displayName: "unlock",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/unlock",
};

11
libs/unlock/package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/unlock",
"version": "0.0.1",
"description": "Unlock the account of a user",
"private": true,
"type": "commonjs",
"main": "index.js",
"types": "index.d.ts",
"license": "GPL-3.0",
"author": "key-management"
}

34
libs/unlock/project.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "unlock",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/unlock/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/unlock",
"main": "libs/unlock/src/index.ts",
"tsConfig": "libs/unlock/tsconfig.lib.json",
"assets": ["libs/unlock/*.md"],
"rootDir": "libs/unlock/src"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/unlock/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/unlock/jest.config.js"
}
}
}
}

View File

@@ -0,0 +1 @@
export { UnlockService } from "./unlock.service";

View File

@@ -0,0 +1,19 @@
import { UserId } from "@bitwarden/common/types/guid";
import { PinLockType } from "@bitwarden/common/key-management/pin/pin-lock-type";
/**
* Service for unlocking a user's account with various methods.
*/
export abstract class UnlockService {
/**
* Unlocks the user's account using their PIN.
*
* @param userId - The user's id
* @param pin - The user's PIN
* @param pinLockType - The type of PIN lock (PERSISTENT or EPHEMERAL)
* @throws If the SDK is not available
* @throws If the PIN is invalid or decryption fails
*/
abstract unlockWithPin(userId: UserId, pin: string, pinLockType: PinLockType): Promise<void>;
}

5
libs/unlock/src/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export { UnlockService } from "./abstractions/unlock.service";
export { DefaultUnlockService } from "./unlock.service";
// Re-export abstractions
export * from "./abstractions";

View File

@@ -0,0 +1,201 @@
import { first, firstValueFrom, map } from "rxjs";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { PinLockType } from "@bitwarden/common/key-management/pin/pin-lock-type";
import { PinStateServiceAbstraction } from "@bitwarden/common/key-management/pin/pin-state.service.abstraction";
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
import { UserId } from "@bitwarden/common/types/guid";
import { UnlockService } from "./abstractions/unlock.service";
import { KdfConfig, KdfConfigService } from "@bitwarden/key-management";
import { EncString, Kdf, MasterPasswordUnlockData, PasswordProtectedKeyEnvelope, UnsignedSharedKey, WrappedAccountCryptographicState } from "@bitwarden/sdk-internal";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import {
asUuid,
} from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DeviceKey } from "@bitwarden/common/types/key";
export class DefaultUnlockService implements UnlockService {
constructor(
private registerSdkService: RegisterSdkService,
private accountCryptographicStateService: AccountCryptographicStateService,
private pinStateService: PinStateServiceAbstraction,
private kdfService: KdfConfigService,
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private apiService: ApiService,
) { }
private async getAccountCryptographicState(userId: UserId): Promise<WrappedAccountCryptographicState> {
return firstValueFrom(
this.accountCryptographicStateService.accountCryptographicState$(userId),
);
}
private async getKdfParams(userId: UserId): Promise<Kdf> {
return firstValueFrom(
this.kdfService.getKdfConfig$(userId).pipe(
map((config: KdfConfig) => {
return config.toSdkConfig();
}),
),
);
}
private async getEmail(userId: UserId): Promise<string> {
return await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
}
private async getPinProtectedUserKeyEnvelope(userId: UserId): Promise<PasswordProtectedKeyEnvelope | null> {
const pinLockType = await this.pinStateService.getPinLockType(userId);
return this.pinStateService.getPinProtectedUserKeyEnvelope(
userId,
pinLockType,
);
}
private async getMasterPasswordUnlockData(userId: UserId): Promise<MasterPasswordUnlockData | null> {
const unlockData = await firstValueFrom(this.masterPasswordService.masterPasswordUnlockData$(userId));
return unlockData.toSdk();
}
async unlockWithDeviceKey(userId: UserId,
encryptedDevicePrivateKey: EncString,
encryptedUserKey: UnsignedSharedKey,
deviceKey: DeviceKey,
): Promise<void> {
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId),
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
deviceKey: {
device_key: deviceKey.toBase64(),
protected_device_private_key: encryptedDevicePrivateKey,
device_protected_user_key: encryptedUserKey,
},
},
});
}),
),
);
}
async unlockWithAuthRequest(userId: UserId, privateKey: string, protectedUserKey: UnsignedSharedKey): Promise<void> {
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId),
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
authRequest: {
request_private_key: privateKey,
method: {
userKey: {
protected_user_key: protectedUserKey,
}
}
},
},
});
}),
),
);
}
async unlockWithKeyConnector(userId: UserId, keyConnectorUrl: string): Promise<void> {
const keyConnectorKey = (await this.apiService.getMasterKeyFromKeyConnector(keyConnectorUrl)).key;
const keyConnectorKeyWrappedUserKey = await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId),
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
keyConnector: {
master_key: keyConnectorKey,
user_key: keyConnectorKeyWrappedUserKey.toSdk(),
},
},
});
}),
),
);
}
async unlockWithPin(userId: UserId, pin: string): Promise<void> {
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId),
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
pinEnvelope: {
pin: pin,
pin_protected_user_key_envelope: await this.getPinProtectedUserKeyEnvelope(userId),
},
},
});
}),
),
);
}
async unlockWithMasterPassword(userId: UserId, masterPassword: string): Promise<void> {
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId),
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
masterPasswordUnlock: {
password: masterPassword,
master_password_unlock: await this.getMasterPasswordUnlockData(userId),
},
},
});
}),
),
);
}
}

View File

@@ -0,0 +1,8 @@
import * as lib from "./index";
describe("unlock", () => {
// This test will fail until something is exported from index.ts
it("should work", () => {
expect(lib).toBeDefined();
});
});

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["**/build", "**/dist"]
}

13
libs/unlock/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

9
package-lock.json generated
View File

@@ -683,6 +683,11 @@
"version": "0.0.0",
"license": "GPL-3.0"
},
"libs/unlock": {
"name": "@bitwarden/unlock",
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/user-core": {
"name": "@bitwarden/user-core",
"version": "0.0.0",
@@ -5143,6 +5148,10 @@
"resolved": "libs/ui/common",
"link": true
},
"node_modules/@bitwarden/unlock": {
"resolved": "libs/unlock",
"link": true
},
"node_modules/@bitwarden/user-core": {
"resolved": "libs/user-core",
"link": true

View File

@@ -30,8 +30,8 @@
"@bitwarden/browser/*": ["./apps/browser/src/*"],
"@bitwarden/cli/*": ["./apps/cli/src/*"],
"@bitwarden/client-type": ["./libs/client-type/src/index.ts"],
"@bitwarden/common/spec": ["./libs/common/spec"],
"@bitwarden/common/*": ["./libs/common/src/*"],
"@bitwarden/common/spec": ["./libs/common/spec"],
"@bitwarden/components": ["./libs/components/src"],
"@bitwarden/core-test-utils": ["./libs/core-test-utils/src/index.ts"],
"@bitwarden/dirt-card": ["./libs/dirt/card/src"],
@@ -62,6 +62,7 @@
"@bitwarden/subscription": ["./libs/subscription/src/index.ts"],
"@bitwarden/ui-common": ["./libs/ui/common/src"],
"@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"],
"@bitwarden/unlock": ["./libs/unlock/src/index.ts"],
"@bitwarden/user-core": ["./libs/user-core/src/index.ts"],
"@bitwarden/vault": ["./libs/vault/src"],
"@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"],