[SM-1800] Handle corrupted encrypted project names gracefully - Clients (#18873)

* Handle corrupted encrypted project names gracefully

- Add decryptionError flag to ProjectView, ProjectListView, and SecretProjectView
- Wrap all project name decryption in try-catch blocks across services
- Use DECRYPT_ERROR constant instead of hardcoded strings
- Display corrupted names as clickable warning text in projects list
- Show warning badges for corrupted project names in secrets list
- Add i18n keys for error tooltips
- Add keyboard accessibility (role, tabindex, enter/space handlers)

* fixing tests

* fixing jittery loading
This commit is contained in:
cd-bitwarden 2026-03-11 17:42:36 -04:00 committed by GitHub
parent 2d702e0eac
commit 79c52ca29b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 111 additions and 31 deletions

View File

@ -8581,6 +8581,14 @@
"message": "New project",
"description": "Title for creating a new project."
},
"clickToRenameProject": {
"message": "Click to rename this project",
"description": "Tooltip text for corrupted project names that can be clicked to rename."
},
"cannotDecryptProjectName": {
"message": "Cannot decrypt project name - click project to rename",
"description": "Tooltip text for project badges when the project name cannot be decrypted."
},
"softDeleteSecretWarning": {
"message": "Deleting secrets can affect existing integrations.",
"description": "Warns that deleting secrets can have consequences on integrations"

View File

@ -9,4 +9,5 @@ export class ProjectListView {
read: boolean;
write: boolean;
linkable: boolean;
decryptionError: boolean = false;
}

View File

@ -8,4 +8,5 @@ export class ProjectView {
revisionDate: string;
read: boolean;
write: boolean;
decryptionError: boolean = false;
}

View File

@ -3,4 +3,5 @@
export class SecretProjectView {
id: string;
name: string;
decryptionError: boolean = false;
}

View File

@ -48,7 +48,9 @@ export class ProjectDialogComponent implements OnInit {
private platformUtilsService: PlatformUtilsService,
private router: Router,
private toastService: ToastService,
) {}
) {
this.loading = data.operation === OperationType.Edit;
}
async ngOnInit() {
if (this.data.operation === OperationType.Edit && this.data.projectId) {

View File

@ -7,7 +7,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import {
DECRYPT_ERROR,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationId } from "@bitwarden/common/types/guid";
@ -136,10 +139,15 @@ export class ProjectService {
projectView.revisionDate = projectResponse.revisionDate;
projectView.read = projectResponse.read;
projectView.write = projectResponse.write;
projectView.name = await this.encryptService.decryptString(
new EncString(projectResponse.name),
orgKey,
);
try {
projectView.name = await this.encryptService.decryptString(
new EncString(projectResponse.name),
orgKey,
);
} catch {
projectView.name = DECRYPT_ERROR;
projectView.decryptionError = true;
}
return projectView;
}
@ -155,10 +163,15 @@ export class ProjectService {
projectListView.organizationId = s.organizationId;
projectListView.read = s.read;
projectListView.write = s.write;
projectListView.name = await this.encryptService.decryptString(
new EncString(s.name),
orgKey,
);
try {
projectListView.name = await this.encryptService.decryptString(
new EncString(s.name),
orgKey,
);
} catch {
projectListView.name = DECRYPT_ERROR;
projectListView.decryptionError = true;
}
projectListView.creationDate = s.creationDate;
projectListView.revisionDate = s.revisionDate;
projectListView.linkable = true;

View File

@ -130,6 +130,7 @@ const secretView: SecretView = {
{
id: "502d93ae-a084-490a-8a64-b187015eb69c",
name: "project-name",
decryptionError: false,
},
],
read: true,
@ -156,6 +157,7 @@ const expectedSecretView: SecretView = {
{
id: "502d93ae-a084-490a-8a64-b187015eb69c",
name: mockUnencryptedData,
decryptionError: false,
},
],
read: true,

View File

@ -7,7 +7,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import {
DECRYPT_ERROR,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { KeyService } from "@bitwarden/key-management";
@ -270,9 +273,14 @@ export class SecretService {
projects.map(async (s: SecretProjectResponse) => {
const projectsMappedToSecretView = new SecretProjectView();
projectsMappedToSecretView.id = s.id;
projectsMappedToSecretView.name = s.name
? await this.encryptService.decryptString(new EncString(s.name), orgKey)
: null;
try {
projectsMappedToSecretView.name = s.name
? await this.encryptService.decryptString(new EncString(s.name), orgKey)
: null;
} catch {
projectsMappedToSecretView.name = DECRYPT_ERROR;
projectsMappedToSecretView.decryptionError = true;
}
return projectsMappedToSecretView;
}),
);

View File

@ -7,7 +7,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import {
DECRYPT_ERROR,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationId } from "@bitwarden/common/types/guid";
@ -147,7 +150,11 @@ export class SecretsManagerPortingApiService {
exportData.projects.map(async (p) => {
const project = new SecretsManagerExportProject();
project.id = p.id;
project.name = await this.encryptService.decryptString(new EncString(p.name), orgKey);
try {
project.name = await this.encryptService.decryptString(new EncString(p.name), orgKey);
} catch {
project.name = DECRYPT_ERROR;
}
return project;
}),
);

View File

@ -7,7 +7,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import {
DECRYPT_ERROR,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationId } from "@bitwarden/common/types/guid";
@ -359,15 +362,21 @@ export class AccessPolicyService {
organizationKey: SymmetricCryptoKey,
response: GrantedProjectAccessPolicyResponse,
): Promise<GrantedProjectAccessPolicyView> {
let projectName = null;
if (response.grantedProjectName) {
try {
projectName = await this.encryptService.decryptString(
new EncString(response.grantedProjectName),
organizationKey,
);
} catch {
projectName = DECRYPT_ERROR;
}
}
return {
...this.createBaseAccessPolicyView(response),
grantedProjectId: response.grantedProjectId,
grantedProjectName: response.grantedProjectName
? await this.encryptService.decryptString(
new EncString(response.grantedProjectName),
organizationKey,
)
: null,
grantedProjectName: projectName,
};
}
@ -432,9 +441,15 @@ export class AccessPolicyService {
view.currentUserInGroup = r.currentUserInGroup;
if (r.type === "serviceAccount" || r.type === "project") {
view.name = r.name
? await this.encryptService.decryptString(new EncString(r.name), orgKey)
: null;
if (r.name) {
try {
view.name = await this.encryptService.decryptString(new EncString(r.name), orgKey);
} catch {
view.name = DECRYPT_ERROR;
}
} else {
view.name = null;
}
} else {
view.name = r.name;
}

View File

@ -62,13 +62,30 @@
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
<i class="bwi bwi-collection tw-text-muted" aria-hidden="true"></i>
<div>
<span
*ngIf="project.decryptionError"
class="tw-cursor-pointer tw-text-warning tw-underline"
role="button"
tabindex="0"
[title]="'clickToRenameProject' | i18n"
[attr.aria-label]="'clickToRenameProject' | i18n"
(click)="editProjectEvent.emit(project.id); $event.stopPropagation()"
(keydown.enter)="editProjectEvent.emit(project.id); $event.stopPropagation()"
(keydown.space)="
editProjectEvent.emit(project.id); $event.stopPropagation(); $event.preventDefault()
"
>
{{ project.name }}
<i class="bwi bwi-pencil-square tw-ml-1" aria-hidden="true"></i>
</span>
<a
*ngIf="project.linkable"
*ngIf="!project.decryptionError && project.linkable"
bitLink
[routerLink]="['/sm', project.organizationId, 'projects', project.id]"
>{{ project.name }}</a
>
<span *ngIf="!project.linkable">{{ project.name }}</span>
{{ project.name }}
</a>
<span *ngIf="!project.decryptionError && !project.linkable">{{ project.name }}</span>
<div class="tw-text-sm tw-text-muted tw-block">
{{ project.id }}
<button

View File

@ -85,11 +85,16 @@
<span
*ngFor="let project of row.projects"
bitBadge
variant="secondary"
[variant]="project.decryptionError ? 'warning' : 'secondary'"
class="tw-ml-1"
[title]="project.name"
[title]="project.decryptionError ? ('cannotDecryptProjectName' | i18n) : project.name"
maxWidthClass="tw-max-w-60"
>
<i
*ngIf="project.decryptionError"
class="bwi bwi-exclamation-triangle tw-mr-1"
aria-hidden="true"
></i>
{{ project.name }}
</span>
<span *ngIf="row.projects.length === 0" bitBadge variant="warning" class="tw-ml-1"