1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-05 18:13:26 +00:00

[PM-5499] Create Auth Request Service (#8056)

* create auth request service

* copy methods from auth crypto service

* register new auth request service

* remove refs to auth request crypto service

* remove auth request crypto service

* remove passwordless login method from login strategy service

* add docs to auth request service
This commit is contained in:
Jake Fink
2024-02-26 10:07:08 -05:00
committed by GitHub
parent d02651583f
commit 1435203e12
23 changed files with 381 additions and 274 deletions

View File

@@ -0,0 +1,191 @@
import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { AuthRequestService } from "./auth-request.service";
describe("AuthRequestService", () => {
let sut: AuthRequestService;
const appIdService = mock<AppIdService>();
const cryptoService = mock<CryptoService>();
const apiService = mock<ApiService>();
const stateService = mock<StateService>();
let mockPrivateKey: Uint8Array;
beforeEach(() => {
jest.clearAllMocks();
sut = new AuthRequestService(appIdService, cryptoService, apiService, stateService);
mockPrivateKey = new Uint8Array(64);
});
describe("approveOrDenyAuthRequest", () => {
beforeEach(() => {
cryptoService.rsaEncrypt.mockResolvedValue({
encryptedString: "ENCRYPTED_STRING",
} as EncString);
appIdService.getAppId.mockResolvedValue("APP_ID");
});
it("should throw if auth request is missing id or key", async () => {
const authRequestNoId = new AuthRequestResponse({ id: "", key: "KEY" });
const authRequestNoKey = new AuthRequestResponse({ id: "123", key: "" });
await expect(sut.approveOrDenyAuthRequest(true, authRequestNoId)).rejects.toThrow(
"Auth request has no id",
);
await expect(sut.approveOrDenyAuthRequest(true, authRequestNoKey)).rejects.toThrow(
"Auth request has no public key",
);
});
it("should use the master key and hash if they exist", async () => {
cryptoService.getMasterKey.mockResolvedValueOnce({ encKey: new Uint8Array(64) } as MasterKey);
stateService.getKeyHash.mockResolvedValueOnce("KEY_HASH");
await sut.approveOrDenyAuthRequest(true, new AuthRequestResponse({ id: "123", key: "KEY" }));
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
});
it("should use the user key if the master key and hash do not exist", async () => {
cryptoService.getUserKey.mockResolvedValueOnce({ key: new Uint8Array(64) } as UserKey);
await sut.approveOrDenyAuthRequest(true, new AuthRequestResponse({ id: "123", key: "KEY" }));
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything());
});
});
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
it("decrypts and sets user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedUserKey",
} as AuthRequestResponse;
const mockDecryptedUserKey = {} as UserKey;
jest.spyOn(sut, "decryptPubKeyEncryptedUserKey").mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await sut.setUserKeyAfterDecryptingSharedUserKey(mockAuthReqResponse, mockPrivateKey);
// Assert
expect(sut.decryptPubKeyEncryptedUserKey).toBeCalledWith(
mockAuthReqResponse.key,
mockPrivateKey,
);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("setKeysAfterDecryptingSharedMasterKeyAndHash", () => {
it("decrypts and sets master key and hash and user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedMasterKey",
masterPasswordHash: "authReqPublicKeyEncryptedMasterKeyHash",
} as AuthRequestResponse;
const mockDecryptedMasterKey = {} as MasterKey;
const mockDecryptedMasterKeyHash = "mockDecryptedMasterKeyHash";
const mockDecryptedUserKey = {} as UserKey;
jest.spyOn(sut, "decryptPubKeyEncryptedMasterKeyAndHash").mockResolvedValueOnce({
masterKey: mockDecryptedMasterKey,
masterKeyHash: mockDecryptedMasterKeyHash,
});
cryptoService.setMasterKey.mockResolvedValueOnce(undefined);
cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await sut.setKeysAfterDecryptingSharedMasterKeyAndHash(mockAuthReqResponse, mockPrivateKey);
// Assert
expect(sut.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith(
mockAuthReqResponse.key,
mockAuthReqResponse.masterPasswordHash,
mockPrivateKey,
);
expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash);
expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedUserKey", () => {
it("returns a decrypted user key when given valid public key encrypted user key and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedUserKey = "pubKeyEncryptedUserKey";
const mockDecryptedUserKeyBytes = new Uint8Array(64);
const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey;
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
// Act
const result = await sut.decryptPubKeyEncryptedUserKey(
mockPubKeyEncryptedUserKey,
mockPrivateKey,
);
// Assert
expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey);
expect(result).toEqual(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedMasterKeyAndHash", () => {
it("returns a decrypted master key and hash when given a valid public key encrypted master key, public key encrypted master key hash, and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedMasterKey = "pubKeyEncryptedMasterKey";
const mockPubKeyEncryptedMasterKeyHash = "pubKeyEncryptedMasterKeyHash";
const mockDecryptedMasterKeyBytes = new Uint8Array(64);
const mockDecryptedMasterKey = new SymmetricCryptoKey(
mockDecryptedMasterKeyBytes,
) as MasterKey;
const mockDecryptedMasterKeyHashBytes = new Uint8Array(64);
const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes);
cryptoService.rsaDecrypt
.mockResolvedValueOnce(mockDecryptedMasterKeyBytes)
.mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes);
// Act
const result = await sut.decryptPubKeyEncryptedMasterKeyAndHash(
mockPubKeyEncryptedMasterKey,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey,
);
// Assert
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
1,
mockPubKeyEncryptedMasterKey,
mockPrivateKey,
);
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
2,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey,
);
expect(result.masterKey).toEqual(mockDecryptedMasterKey);
expect(result.masterKeyHash).toEqual(mockDecryptedMasterKeyHash);
});
});
});

View File

@@ -0,0 +1,129 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction";
export class AuthRequestService implements AuthRequestServiceAbstraction {
constructor(
private appIdService: AppIdService,
private cryptoService: CryptoService,
private apiService: ApiService,
private stateService: StateService,
) {}
async approveOrDenyAuthRequest(
approve: boolean,
authRequest: AuthRequestResponse,
): Promise<AuthRequestResponse> {
if (!authRequest.id) {
throw new Error("Auth request has no id");
}
if (!authRequest.key) {
throw new Error("Auth request has no public key");
}
const pubKey = Utils.fromB64ToArray(authRequest.key);
const masterKey = await this.cryptoService.getMasterKey();
const masterKeyHash = await this.stateService.getKeyHash();
let encryptedMasterKeyHash;
let keyToEncrypt;
if (masterKey && masterKeyHash) {
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey,
);
keyToEncrypt = masterKey.encKey;
} else {
const userKey = await this.cryptoService.getUserKey();
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
const response = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterKeyHash?.encryptedString,
await this.appIdService.getAppId(),
approve,
);
return await this.apiService.putAuthRequest(authRequest.id, response);
}
async setUserKeyAfterDecryptingSharedUserKey(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array,
) {
const userKey = await this.decryptPubKeyEncryptedUserKey(
authReqResponse.key,
authReqPrivateKey,
);
await this.cryptoService.setUserKey(userKey);
}
async setKeysAfterDecryptingSharedMasterKeyAndHash(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array,
) {
const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash(
authReqResponse.key,
authReqResponse.masterPasswordHash,
authReqPrivateKey,
);
// Decrypt and set user key in state
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
await this.cryptoService.setMasterKey(masterKey);
await this.cryptoService.setMasterKeyHash(masterKeyHash);
await this.cryptoService.setUserKey(userKey);
}
// Decryption helpers
async decryptPubKeyEncryptedUserKey(
pubKeyEncryptedUserKey: string,
privateKey: Uint8Array,
): Promise<UserKey> {
const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedUserKey,
privateKey,
);
return new SymmetricCryptoKey(decryptedUserKeyBytes) as UserKey;
}
async decryptPubKeyEncryptedMasterKeyAndHash(
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: Uint8Array,
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKey,
privateKey,
);
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKeyHash,
privateKey,
);
const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey;
const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer);
return {
masterKey,
masterKeyHash,
};
}
}

View File

@@ -1,2 +1,3 @@
export * from "./pin-crypto/pin-crypto.service.implementation";
export * from "./login-strategies/login-strategy.service";
export * from "./auth-request/auth-request.service";

View File

@@ -2,7 +2,6 @@ import { Observable, Subject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
@@ -11,8 +10,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
@@ -26,11 +23,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { MasterKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../../abstractions";
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy";
import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy";
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
@@ -110,7 +106,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authReqCryptoService: AuthRequestCryptoServiceAbstraction,
protected authRequestService: AuthRequestServiceAbstraction,
) {}
async logIn(
@@ -160,7 +156,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.twoFactorService,
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authReqCryptoService,
this.authRequestService,
this.i18nService,
);
break;
@@ -290,45 +286,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
return this.pushNotificationSubject.asObservable();
}
async passwordlessLogin(
id: string,
key: string,
requestApproved: boolean,
): Promise<AuthRequestResponse> {
const pubKey = Utils.fromB64ToArray(key);
const masterKey = await this.cryptoService.getMasterKey();
let keyToEncrypt;
let encryptedMasterKeyHash = null;
if (masterKey) {
keyToEncrypt = masterKey.encKey;
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
const masterKeyHash = await this.stateService.getKeyHash();
if (masterKeyHash != null) {
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey,
);
}
} else {
const userKey = await this.cryptoService.getUserKey();
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterKeyHash?.encryptedString,
await this.appIdService.getAppId(),
requestApproved,
);
return await this.apiService.putAuthRequest(id, request);
}
private saveState(
strategy:
| UserApiLoginStrategy