mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-23809] Add simplified interface to MP service (#15631)
* Add new mp service api * Fix tests * Add test coverage * Add newline * Fix type * Rename to "unwrapUserKeyFromMasterPasswordUnlockData" * Fix build * Fix build on cli * Fix linting * Re-sort spec * Add tests * Fix test and build issues * Fix build * Clean up * Remove introduced function * Clean up comments * Fix abstract class types * Fix comments * Cleanup * Cleanup * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/services/master-password.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/types/master-password.types.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add comments * Fix build * Add arg null check * Cleanup * Fix build * Fix build on browser * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests for null params * Cleanup and deprecate more functions * Fix formatting * Prettier * Clean up * Update libs/key-management/src/abstractions/key.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Make emailToSalt private and expose abstract saltForUser * Add tests * Add docs * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/master-password/abstractions/master-password.service.abstraction.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
@@ -668,6 +668,8 @@ export default class MainBackground {
|
||||
this.keyGenerationService,
|
||||
this.encryptService,
|
||||
this.logService,
|
||||
this.cryptoFunctionService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||
|
||||
@@ -431,16 +431,17 @@ export class ServiceContainer {
|
||||
migrationRunner,
|
||||
);
|
||||
|
||||
this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider);
|
||||
this.masterPasswordService = new MasterPasswordService(
|
||||
this.stateProvider,
|
||||
this.stateService,
|
||||
this.keyGenerationService,
|
||||
this.encryptService,
|
||||
this.logService,
|
||||
this.cryptoFunctionService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider);
|
||||
|
||||
this.pinService = new PinService(
|
||||
this.accountService,
|
||||
this.cryptoFunctionService,
|
||||
|
||||
@@ -1021,6 +1021,8 @@ const safeProviders: SafeProvider[] = [
|
||||
KeyGenerationServiceAbstraction,
|
||||
EncryptService,
|
||||
LogService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import {
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../types/master-password.types";
|
||||
|
||||
export abstract class MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
@@ -12,14 +20,23 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract forceSetPasswordReason$: (userId: UserId) => Observable<ForceSetPasswordReason>;
|
||||
/**
|
||||
* An observable that emits the master password salt for the user.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
* @throws If the user ID is provided, but the user is not found.
|
||||
*/
|
||||
abstract saltForUser$: (userId: UserId) => Observable<MasterPasswordSalt>;
|
||||
/**
|
||||
* An observable that emits the master key for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordUnlockData}, {@link makeMasterPasswordAuthenticationData} or {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract masterKey$: (userId: UserId) => Observable<MasterKey>;
|
||||
/**
|
||||
* An observable that emits the master key hash for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordAuthenticationData}.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
@@ -32,6 +49,7 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>;
|
||||
/**
|
||||
* Decrypts the user key with the provided master key
|
||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.
|
||||
* @param masterKey The user's master key
|
||||
* * @param userId The desired user
|
||||
* @param userKey The user's encrypted symmetric key
|
||||
@@ -44,12 +62,52 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
userId: string,
|
||||
userKey?: EncString,
|
||||
) => Promise<UserKey | null>;
|
||||
|
||||
/**
|
||||
* Makes the authentication hash for authenticating to the server with the master password.
|
||||
* @param password The master password.
|
||||
* @param kdf The KDF configuration.
|
||||
* @param salt The master password salt to use. See {@link saltForUser$} for current salt.
|
||||
* @throws If password, KDF or salt are null or undefined.
|
||||
*/
|
||||
abstract makeMasterPasswordAuthenticationData: (
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
) => Promise<MasterPasswordAuthenticationData>;
|
||||
|
||||
/**
|
||||
* Creates a MasterPasswordUnlockData bundle that encrypts the user-key with a key derived from the password. The
|
||||
* bundle also contains the KDF settings and salt used to derive the key, which are required to decrypt the user-key later.
|
||||
* @param password The master password.
|
||||
* @param kdf The KDF configuration.
|
||||
* @param salt The master password salt to use. See {@link saltForUser$} for current salt.
|
||||
* @param userKey The user's userKey to encrypt.
|
||||
* @throws If password, KDF, salt, or userKey are null or undefined.
|
||||
*/
|
||||
abstract makeMasterPasswordUnlockData: (
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
userKey: UserKey,
|
||||
) => Promise<MasterPasswordUnlockData>;
|
||||
|
||||
/**
|
||||
* Unwraps a user-key that was wrapped with a password provided KDF settings. The same KDF settings and salt must be provided to unwrap the user-key, otherwise it will fail to decrypt.
|
||||
* @throws If the encryption type is not supported.
|
||||
* @throws If the password, KDF, or salt don't match the original wrapping parameters.
|
||||
*/
|
||||
abstract unwrapUserKeyFromMasterPasswordUnlockData: (
|
||||
password: string,
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
) => Promise<UserKey>;
|
||||
}
|
||||
|
||||
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
* Set the master key for the user.
|
||||
* Note: Use {@link clearMasterKey} to clear the master key.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param masterKey The master key.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or master key is missing.
|
||||
@@ -57,6 +115,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas
|
||||
abstract setMasterKey: (masterKey: MasterKey, userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Clear the master key for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
@@ -64,6 +123,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas
|
||||
/**
|
||||
* Set the master key hash for the user.
|
||||
* Note: Use {@link clearMasterKeyHash} to clear the master key hash.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param masterKeyHash The master key hash.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or master key hash is missing.
|
||||
@@ -71,6 +131,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas
|
||||
abstract setMasterKeyHash: (masterKeyHash: string, userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Clear the master key hash for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
|
||||
@@ -3,11 +3,20 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { ReplaySubject, Observable } from "rxjs";
|
||||
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../types/master-password.types";
|
||||
|
||||
export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
||||
mock = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
@@ -24,6 +33,10 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||
this.masterKeyHashSubject.next(initialMasterKeyHash);
|
||||
}
|
||||
|
||||
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
|
||||
return this.mock.saltForUser$(userId);
|
||||
}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
return this.masterKeySubject.asObservable();
|
||||
}
|
||||
@@ -71,4 +84,28 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||
): Promise<UserKey> {
|
||||
return this.mock.decryptUserKeyWithMasterKey(masterKey, userId, userKey);
|
||||
}
|
||||
|
||||
makeMasterPasswordAuthenticationData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
): Promise<MasterPasswordAuthenticationData> {
|
||||
return this.mock.makeMasterPasswordAuthenticationData(password, kdf, salt);
|
||||
}
|
||||
|
||||
makeMasterPasswordUnlockData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
userKey: UserKey,
|
||||
): Promise<MasterPasswordUnlockData> {
|
||||
return this.mock.makeMasterPasswordUnlockData(password, kdf, salt, userKey);
|
||||
}
|
||||
|
||||
unwrapUserKeyFromMasterPasswordUnlockData(
|
||||
password: string,
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
): Promise<UserKey> {
|
||||
return this.mock.unwrapUserKeyFromMasterPasswordUnlockData(password, masterPasswordUnlockData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import * as rxjs from "rxjs";
|
||||
|
||||
import { makeSymmetricCryptoKey } from "../../../../spec";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../spec";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
@@ -10,9 +19,11 @@ import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import { MasterPasswordSalt } from "../types/master-password.types";
|
||||
|
||||
import { MasterPasswordService } from "./master-password.service";
|
||||
|
||||
@@ -24,8 +35,10 @@ describe("MasterPasswordService", () => {
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const userId = "user-id" as UserId;
|
||||
const userId = "00000000-0000-0000-0000-000000000000" as UserId;
|
||||
const mockUserState = {
|
||||
state$: of(null),
|
||||
update: jest.fn().mockResolvedValue(null),
|
||||
@@ -45,6 +58,8 @@ describe("MasterPasswordService", () => {
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
logService = mock<LogService>();
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
|
||||
stateProvider.getUser.mockReturnValue(mockUserState as any);
|
||||
|
||||
@@ -56,10 +71,33 @@ describe("MasterPasswordService", () => {
|
||||
keyGenerationService,
|
||||
encryptService,
|
||||
logService,
|
||||
cryptoFunctionService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
encryptService.unwrapSymmetricKey.mockResolvedValue(makeSymmetricCryptoKey(64, 1));
|
||||
keyGenerationService.stretchKey.mockResolvedValue(makeSymmetricCryptoKey(64, 3));
|
||||
Object.defineProperty(SdkLoadService, "Ready", {
|
||||
value: Promise.resolve(),
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("saltForUser$", () => {
|
||||
it("throws when userid not present", async () => {
|
||||
expect(() => {
|
||||
sut.saltForUser$(null as unknown as UserId);
|
||||
}).toThrow("userId is null or undefined.");
|
||||
});
|
||||
it("throws when userid present but not in account service", async () => {
|
||||
await expect(
|
||||
firstValueFrom(sut.saltForUser$("00000000-0000-0000-0000-000000000001" as UserId)),
|
||||
).rejects.toThrow("Cannot read properties of undefined (reading 'email')");
|
||||
});
|
||||
it("returns salt", async () => {
|
||||
const salt = await firstValueFrom(sut.saltForUser$(userId));
|
||||
expect(salt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setForceSetPasswordReason", () => {
|
||||
@@ -190,4 +228,97 @@ describe("MasterPasswordService", () => {
|
||||
expect(updateFn(null)).toEqual(encryptedKey.toJSON());
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeMasterPasswordAuthenticationData", () => {
|
||||
const password = "test-password";
|
||||
const kdf: KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||
const masterKey = makeSymmetricCryptoKey(32, 2);
|
||||
const masterKeyHash = makeSymmetricCryptoKey(32, 3).toEncoded();
|
||||
|
||||
beforeEach(() => {
|
||||
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(masterKeyHash);
|
||||
});
|
||||
|
||||
it("derives master key and creates authentication hash", async () => {
|
||||
const result = await sut.makeMasterPasswordAuthenticationData(password, kdf, salt);
|
||||
|
||||
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(password, salt, kdf);
|
||||
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith(
|
||||
masterKey.toEncoded(),
|
||||
password,
|
||||
"sha256",
|
||||
1,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
kdf,
|
||||
salt,
|
||||
masterPasswordAuthenticationHash: Utils.fromBufferToB64(masterKeyHash),
|
||||
});
|
||||
});
|
||||
|
||||
it("throws if password is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordAuthenticationData(null as unknown as string, kdf, salt),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if kdf is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordAuthenticationData(password, null as unknown as KdfConfig, salt),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if salt is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordAuthenticationData(
|
||||
password,
|
||||
kdf,
|
||||
null as unknown as MasterPasswordSalt,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("wrapUnwrapUserKeyWithPassword", () => {
|
||||
const password = "test-password";
|
||||
const kdf: KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||
const userKey = makeSymmetricCryptoKey(64, 2) as UserKey;
|
||||
|
||||
it("wraps and unwraps user key with password", async () => {
|
||||
const unlockData = await sut.makeMasterPasswordUnlockData(password, kdf, salt, userKey);
|
||||
const unwrappedUserkey = await sut.unwrapUserKeyFromMasterPasswordUnlockData(
|
||||
password,
|
||||
unlockData,
|
||||
);
|
||||
expect(unwrappedUserkey).toEqual(userKey);
|
||||
});
|
||||
|
||||
it("throws if password is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(null as unknown as string, kdf, salt, userKey),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if kdf is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(password, null as unknown as KdfConfig, salt, userKey),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if salt is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(
|
||||
password,
|
||||
kdf,
|
||||
null as unknown as MasterPasswordSalt,
|
||||
userKey,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if userKey is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(password, kdf, salt, null as unknown as UserKey),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
@@ -16,9 +24,17 @@ import {
|
||||
} from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
import { EncryptedString, EncString } from "../../crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordAuthenticationHash,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../types/master-password.types";
|
||||
|
||||
/** Memory since master key shouldn't be available on lock */
|
||||
const MASTER_KEY = new UserKeyDefinition<MasterKey>(MASTER_PASSWORD_MEMORY, "masterKey", {
|
||||
@@ -59,8 +75,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private encryptService: EncryptService,
|
||||
private logService: LogService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
|
||||
assertNonNullish(userId, "userId");
|
||||
return this.accountService.accounts$.pipe(
|
||||
map((accounts) => accounts[userId].email),
|
||||
map((email) => this.emailToSalt(email)),
|
||||
);
|
||||
}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
@@ -95,6 +121,10 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
return EncString.fromJSON(key);
|
||||
}
|
||||
|
||||
private emailToSalt(email: string): MasterPasswordSalt {
|
||||
return email.toLowerCase().trim() as MasterPasswordSalt;
|
||||
}
|
||||
|
||||
async setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> {
|
||||
if (masterKey == null) {
|
||||
throw new Error("Master key is required.");
|
||||
@@ -202,4 +232,89 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
|
||||
return decUserKey as UserKey;
|
||||
}
|
||||
|
||||
async makeMasterPasswordAuthenticationData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
): Promise<MasterPasswordAuthenticationData> {
|
||||
assertNonNullish(password, "password");
|
||||
assertNonNullish(kdf, "kdf");
|
||||
assertNonNullish(salt, "salt");
|
||||
|
||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||
|
||||
const SERVER_AUTHENTICATION_HASH_ITERATIONS = 1;
|
||||
|
||||
const masterKey = (await this.keyGenerationService.deriveKeyFromPassword(
|
||||
password,
|
||||
salt,
|
||||
kdf,
|
||||
)) as MasterKey;
|
||||
|
||||
const masterPasswordAuthenticationHash = Utils.fromBufferToB64(
|
||||
await this.cryptoFunctionService.pbkdf2(
|
||||
masterKey.toEncoded(),
|
||||
password,
|
||||
"sha256",
|
||||
SERVER_AUTHENTICATION_HASH_ITERATIONS,
|
||||
),
|
||||
) as MasterPasswordAuthenticationHash;
|
||||
|
||||
return {
|
||||
salt,
|
||||
kdf,
|
||||
masterPasswordAuthenticationHash,
|
||||
} as MasterPasswordAuthenticationData;
|
||||
}
|
||||
|
||||
async makeMasterPasswordUnlockData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
userKey: UserKey,
|
||||
): Promise<MasterPasswordUnlockData> {
|
||||
assertNonNullish(password, "password");
|
||||
assertNonNullish(kdf, "kdf");
|
||||
assertNonNullish(salt, "salt");
|
||||
assertNonNullish(userKey, "userKey");
|
||||
|
||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||
|
||||
await SdkLoadService.Ready;
|
||||
const masterKeyWrappedUserKey = new EncString(
|
||||
PureCrypto.encrypt_user_key_with_master_password(
|
||||
userKey.toEncoded(),
|
||||
password,
|
||||
salt,
|
||||
kdf.toSdkConfig(),
|
||||
),
|
||||
) as MasterKeyWrappedUserKey;
|
||||
return {
|
||||
salt,
|
||||
kdf,
|
||||
masterKeyWrappedUserKey,
|
||||
};
|
||||
}
|
||||
|
||||
async unwrapUserKeyFromMasterPasswordUnlockData(
|
||||
password: string,
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
): Promise<UserKey> {
|
||||
assertNonNullish(password, "password");
|
||||
assertNonNullish(masterPasswordUnlockData, "masterPasswordUnlockData");
|
||||
|
||||
await SdkLoadService.Ready;
|
||||
const userKey = new SymmetricCryptoKey(
|
||||
PureCrypto.decrypt_user_key_with_master_password(
|
||||
masterPasswordUnlockData.masterKeyWrappedUserKey.encryptedString,
|
||||
password,
|
||||
masterPasswordUnlockData.salt,
|
||||
masterPasswordUnlockData.kdf.toSdkConfig(),
|
||||
),
|
||||
);
|
||||
return userKey as UserKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
|
||||
/**
|
||||
* The Base64-encoded master password authentication hash, that is sent to the server for authentication.
|
||||
*/
|
||||
export type MasterPasswordAuthenticationHash = Opaque<string, "MasterPasswordAuthenticationHash">;
|
||||
/**
|
||||
* You MUST obtain this through the emailToSalt function in MasterPasswordService
|
||||
*/
|
||||
export type MasterPasswordSalt = Opaque<string, "MasterPasswordSalt">;
|
||||
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterPasswordSalt">;
|
||||
|
||||
/**
|
||||
* The data required to unlock with the master password.
|
||||
*/
|
||||
export type MasterPasswordUnlockData = {
|
||||
salt: MasterPasswordSalt;
|
||||
kdf: KdfConfig;
|
||||
masterKeyWrappedUserKey: MasterKeyWrappedUserKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* The data required to authenticate with the master password.
|
||||
*/
|
||||
export type MasterPasswordAuthenticationData = {
|
||||
salt: MasterPasswordSalt;
|
||||
kdf: KdfConfig;
|
||||
masterPasswordAuthenticationHash: MasterPasswordAuthenticationHash;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { SymmetricCryptoKey } from "../platform/models/domain/symmetric-crypto-k
|
||||
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
|
||||
export type PrfKey = Opaque<SymmetricCryptoKey, "PrfKey">;
|
||||
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
|
||||
/** @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. */
|
||||
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
|
||||
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
|
||||
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
|
||||
|
||||
@@ -150,11 +150,18 @@ export abstract class KeyService {
|
||||
|
||||
/**
|
||||
* Generates a new user key
|
||||
* @deprecated Interacting with the master key directly is prohibited. Use {@link makeUserKeyV1} instead.
|
||||
* @throws Error when master key is null and there is no active user
|
||||
* @param masterKey The user's master key. When null, grabs master key from active user.
|
||||
* @returns A new user key and the master key protected version of it
|
||||
*/
|
||||
abstract makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]>;
|
||||
/**
|
||||
* Generates a new user key for a V1 user
|
||||
* Note: This will be replaced by a higher level function to initialize a whole users cryptographic state in the near future.
|
||||
* @returns A new user key
|
||||
*/
|
||||
abstract makeUserKeyV1(): Promise<UserKey>;
|
||||
/**
|
||||
* Clears the user's stored version of the user key
|
||||
* @param keySuffix The desired version of the key to clear
|
||||
@@ -166,6 +173,7 @@ export abstract class KeyService {
|
||||
* Retrieves the user's master key if it is in state, or derives it from the provided password
|
||||
* @param password The user's master password that will be used to derive a master key if one isn't found
|
||||
* @param userId The desired user
|
||||
* @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead.
|
||||
* @throws Error when userId is null/undefined.
|
||||
* @throws Error when email or Kdf configuration cannot be found for the user.
|
||||
* @returns The user's master key if it exists, or a newly derived master key.
|
||||
@@ -173,6 +181,7 @@ export abstract class KeyService {
|
||||
abstract getOrDeriveMasterKey(password: string, userId: UserId): Promise<MasterKey>;
|
||||
/**
|
||||
* Generates a master key from the provided password
|
||||
* @deprecated Interacting with the master key directly is prohibited.
|
||||
* @param password The user's master password
|
||||
* @param email The user's email
|
||||
* @param KdfConfig The user's key derivation function configuration
|
||||
@@ -182,6 +191,7 @@ export abstract class KeyService {
|
||||
/**
|
||||
* Encrypts the existing (or provided) user key with the
|
||||
* provided master key
|
||||
* @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead.
|
||||
* @param masterKey The user's master key
|
||||
* @param userKey The user key
|
||||
* @returns The user key and the master key protected version of it
|
||||
@@ -194,6 +204,7 @@ export abstract class KeyService {
|
||||
* Creates a master password hash from the user's master password. Can
|
||||
* be used for local authentication or for server authentication depending
|
||||
* on the hashPurpose provided.
|
||||
* @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead.
|
||||
* @param password The user's master password
|
||||
* @param key The user's master key or active's user master key.
|
||||
* @param hashPurpose The iterations to use for the hash. Defaults to {@link HashPurpose.ServerAuthorization}.
|
||||
@@ -207,6 +218,7 @@ export abstract class KeyService {
|
||||
): Promise<string>;
|
||||
/**
|
||||
* Compares the provided master password to the stored password hash.
|
||||
* @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead.
|
||||
* @param masterPassword The user's master password
|
||||
* @param masterKey The user's master key
|
||||
* @param userId The id of the user to do the operation for.
|
||||
|
||||
@@ -232,6 +232,11 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return this.buildProtectedSymmetricKey(masterKey, newUserKey);
|
||||
}
|
||||
|
||||
async makeUserKeyV1(): Promise<UserKey> {
|
||||
const newUserKey = await this.keyGenerationService.createKey(512);
|
||||
return newUserKey as UserKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
|
||||
* @param userId The desired user
|
||||
@@ -259,6 +264,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use `makeMasterPasswordAuthenticationData`, `unwrapUserKeyFromMasterPasswordUnlockData` or `makeMasterPasswordUnlockData` in @link MasterPasswordService instead.
|
||||
*/
|
||||
async getOrDeriveMasterKey(password: string, userId: UserId): Promise<MasterKey> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
@@ -287,6 +295,8 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
/**
|
||||
* Derive a master key from a password and email.
|
||||
*
|
||||
* @deprecated Please use `makeMasterPasswordAuthenticationData`, `makeMasterPasswordAuthenticationData`, `unwrapUserKeyFromMasterPasswordUnlockData` in @link MasterPasswordService instead.
|
||||
*
|
||||
* @remarks
|
||||
* Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type.
|
||||
*/
|
||||
@@ -304,6 +314,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use `makeMasterPasswordUnlockData` in {@link MasterPasswordService} instead.
|
||||
*/
|
||||
async encryptUserKeyWithMasterKey(
|
||||
masterKey: MasterKey,
|
||||
userKey?: UserKey,
|
||||
@@ -312,6 +325,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return await this.buildProtectedSymmetricKey(masterKey, userKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use `makeMasterPasswordAuthenticationData` in {@link MasterPasswordService} instead.
|
||||
*/
|
||||
async hashMasterKey(
|
||||
password: string,
|
||||
key: MasterKey,
|
||||
|
||||
Reference in New Issue
Block a user