mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 22:44:11 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -1794,6 +1794,15 @@
|
||||
"passwordHistory": {
|
||||
"message": "Password history"
|
||||
},
|
||||
"generatorHistory": {
|
||||
"message": "Generator history"
|
||||
},
|
||||
"clearGeneratorHistoryTitle": {
|
||||
"message": "Clear generator history"
|
||||
},
|
||||
"cleargGeneratorHistoryDescription": {
|
||||
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
|
||||
},
|
||||
"back": {
|
||||
"message": "Back"
|
||||
},
|
||||
@@ -1910,11 +1919,11 @@
|
||||
"clearHistory": {
|
||||
"message": "Clear history"
|
||||
},
|
||||
"noPasswordsToShow": {
|
||||
"message": "No passwords to show"
|
||||
"nothingToShow": {
|
||||
"message": "Nothing to show"
|
||||
},
|
||||
"noRecentlyGeneratedPassword": {
|
||||
"message": "You haven't generated a password recently"
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "You haven't generated anything recently"
|
||||
},
|
||||
"remove": {
|
||||
"message": "Remove"
|
||||
|
||||
@@ -48,8 +48,12 @@ describe("LocalBackedSessionStorage", () => {
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
const result = await sut.get("test");
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
|
||||
expect(result).toEqual("decrypted");
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
encrypted,
|
||||
sessionKey,
|
||||
"browser-session-key",
|
||||
),
|
||||
expect(result).toEqual("decrypted");
|
||||
});
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
@@ -65,8 +69,12 @@ describe("LocalBackedSessionStorage", () => {
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
const result = await sut.get("test");
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
|
||||
expect(result).toEqual("decrypted");
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
encrypted,
|
||||
sessionKey,
|
||||
"browser-session-key",
|
||||
),
|
||||
expect(result).toEqual("decrypted");
|
||||
});
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
|
||||
@@ -117,7 +117,11 @@ export class LocalBackedSessionStorageService
|
||||
return null;
|
||||
}
|
||||
|
||||
const valueJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey);
|
||||
const valueJson = await this.encryptService.decryptToUtf8(
|
||||
new EncString(local),
|
||||
encKey,
|
||||
"browser-session-key",
|
||||
);
|
||||
if (valueJson == null) {
|
||||
// error with decryption, value is lost, delete state and start over
|
||||
await this.localStorage.remove(this.sessionStorageKey(key));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'passwordHistory' | i18n" showBackButton>
|
||||
<popup-header slot="header" [pageTitle]="'generatorHistory' | i18n" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { BehaviorSubject, distinctUntilChanged, firstValueFrom, map, switchMap }
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ButtonModule, ContainerComponent } from "@bitwarden/components";
|
||||
import { ButtonModule, ContainerComponent, DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorHistoryComponent as CredentialGeneratorHistoryToolsComponent,
|
||||
EmptyCredentialHistoryComponent,
|
||||
@@ -42,6 +42,7 @@ export class CredentialGeneratorHistoryComponent {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private history: GeneratorHistoryService,
|
||||
private dialogService: DialogService,
|
||||
) {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
@@ -61,6 +62,16 @@ export class CredentialGeneratorHistoryComponent {
|
||||
}
|
||||
|
||||
clear = async () => {
|
||||
await this.history.clear(await firstValueFrom(this.userId$));
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "clearGeneratorHistoryTitle" },
|
||||
content: { key: "cleargGeneratorHistoryDescription" },
|
||||
type: "warning",
|
||||
acceptButtonText: { key: "clearHistory" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
await this.history.clear(await firstValueFrom(this.userId$));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<tools-credential-generator />
|
||||
<bit-item>
|
||||
<a type="button" bit-item-content routerLink="/generator-history">
|
||||
{{ "passwordHistory" | i18n }}
|
||||
{{ "generatorHistory" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
|
||||
@@ -455,6 +455,7 @@ export class GetCommand extends DownloadCommand {
|
||||
decCollection.name = await this.encryptService.decryptToUtf8(
|
||||
new EncString(response.name),
|
||||
orgKey,
|
||||
`orgkey-${options.organizationId}`,
|
||||
);
|
||||
const groups =
|
||||
response.groups == null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router, UrlTree } from "@angular/router";
|
||||
import { Navigation, Router, UrlTree } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -37,19 +37,29 @@ describe("unauthUiRefreshRedirect", () => {
|
||||
expect(router.parseUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns UrlTree when UnauthenticatedExtensionUIRefresh flag is enabled", async () => {
|
||||
const mockUrlTree = mock<UrlTree>();
|
||||
it("returns UrlTree when UnauthenticatedExtensionUIRefresh flag is enabled and preserves query params", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
router.parseUrl.mockReturnValue(mockUrlTree);
|
||||
|
||||
const result = await TestBed.runInInjectionContext(() =>
|
||||
unauthUiRefreshRedirect("/redirect")(),
|
||||
);
|
||||
const queryParams = { test: "test" };
|
||||
|
||||
const navigation: Navigation = {
|
||||
extras: {
|
||||
queryParams: queryParams,
|
||||
},
|
||||
id: 0,
|
||||
initialUrl: new UrlTree(),
|
||||
extractedUrl: new UrlTree(),
|
||||
trigger: "imperative",
|
||||
previousNavigation: undefined,
|
||||
};
|
||||
|
||||
router.getCurrentNavigation.mockReturnValue(navigation);
|
||||
|
||||
await TestBed.runInInjectionContext(() => unauthUiRefreshRedirect("/redirect")());
|
||||
|
||||
expect(result).toBe(mockUrlTree);
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
expect(router.parseUrl).toHaveBeenCalledWith("/redirect");
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], { queryParams });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,12 @@ export function unauthUiRefreshRedirect(redirectUrl: string): () => Promise<bool
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
if (shouldRedirect) {
|
||||
return router.parseUrl(redirectUrl);
|
||||
const currentNavigation = router.getCurrentNavigation();
|
||||
const queryParams = currentNavigation?.extras?.queryParams || {};
|
||||
|
||||
// Preserve query params when redirecting as it is likely that the refreshed component
|
||||
// will be consuming the same query params.
|
||||
return router.createUrlTree([redirectUrl], { queryParams });
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,15 @@ export function extensionRefreshRedirect(redirectUrl: string): () => Promise<boo
|
||||
return async () => {
|
||||
const configService = inject(ConfigService);
|
||||
const router = inject(Router);
|
||||
|
||||
const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
|
||||
if (shouldRedirect) {
|
||||
return router.parseUrl(redirectUrl);
|
||||
const currentNavigation = router.getCurrentNavigation();
|
||||
const queryParams = currentNavigation?.extras?.queryParams || {};
|
||||
|
||||
// Preserve query params when redirecting as it is likely that the refreshed component
|
||||
// will be consuming the same query params.
|
||||
return router.createUrlTree([redirectUrl], { queryParams });
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
export abstract class EncryptService {
|
||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
|
||||
abstract decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string>;
|
||||
abstract decryptToUtf8(
|
||||
encString: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptContext?: string,
|
||||
): Promise<string>;
|
||||
abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array>;
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("EncString", () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
|
||||
await encString.decryptWithKey(key, encryptService);
|
||||
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key, "domain-withkey");
|
||||
});
|
||||
|
||||
it("fails to decrypt when key is null", async () => {
|
||||
@@ -351,7 +351,7 @@ describe("EncString", () => {
|
||||
await encString.decrypt(null, key);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key, "provided-key");
|
||||
});
|
||||
|
||||
it("gets an organization key if required", async () => {
|
||||
@@ -362,7 +362,11 @@ describe("EncString", () => {
|
||||
await encString.decrypt("orgId", null);
|
||||
|
||||
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, orgKey);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
encString,
|
||||
orgKey,
|
||||
"domain-orgkey-orgId",
|
||||
);
|
||||
});
|
||||
|
||||
it("gets the user's decryption key if required", async () => {
|
||||
@@ -373,7 +377,11 @@ describe("EncString", () => {
|
||||
await encString.decrypt(null, null);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, userKey);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
encString,
|
||||
userKey,
|
||||
"domain-withlegacysupport-masterkey",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -159,16 +159,27 @@ export class EncString implements Encrypted {
|
||||
return this.decryptedValue;
|
||||
}
|
||||
|
||||
let keyContext = "provided-key";
|
||||
try {
|
||||
if (key == null) {
|
||||
key = await this.getKeyForDecryption(orgId);
|
||||
keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
|
||||
if (orgId != null) {
|
||||
keyContext = `domain-orgkey-${orgId}`;
|
||||
} else {
|
||||
const cryptoService = Utils.getContainerService().getKeyService();
|
||||
keyContext =
|
||||
(await cryptoService.getUserKey()) == null
|
||||
? "domain-withlegacysupport-masterkey"
|
||||
: "domain-withlegacysupport-userkey";
|
||||
}
|
||||
}
|
||||
if (key == null) {
|
||||
throw new Error("No key to decrypt EncString with orgId " + orgId);
|
||||
}
|
||||
|
||||
const encryptService = Utils.getContainerService().getEncryptService();
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(this, key);
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext);
|
||||
} catch (e) {
|
||||
this.decryptedValue = DECRYPT_ERROR;
|
||||
}
|
||||
@@ -181,7 +192,7 @@ export class EncString implements Encrypted {
|
||||
throw new Error("No key to decrypt EncString");
|
||||
}
|
||||
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(this, key);
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey");
|
||||
} catch (e) {
|
||||
this.decryptedValue = DECRYPT_ERROR;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,11 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return new EncArrayBuffer(encBytes);
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
||||
async decryptToUtf8(
|
||||
encString: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptContext: string = "no context",
|
||||
): Promise<string> {
|
||||
if (key == null) {
|
||||
throw new Error("No key provided for decryption.");
|
||||
}
|
||||
@@ -75,8 +79,9 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
"Decrypt context: " + decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -85,8 +90,9 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
"Decrypt context: " + decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -108,8 +114,10 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="tw-flex tw-flex-col tw-h-full tw-justify-center">
|
||||
<div class="tw-text-center">
|
||||
<bit-icon [icon]="noCredentialsIcon" aria-hidden="true"></bit-icon>
|
||||
<h2 bitTypography="h4" class="tw-mt-3">{{ "noPasswordsToShow" | i18n }}</h2>
|
||||
<div>{{ "noRecentlyGeneratedPassword" | i18n }}</div>
|
||||
<h2 bitTypography="h4" class="tw-mt-3">{{ "nothingToShow" | i18n }}</h2>
|
||||
<div>{{ "nothingGeneratedRecently" | i18n }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user