mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 01:33:33 +00:00
[PM-22750] Reimplement fix old attachment logic (#17689)
* [PM-22750] Add upgradeOldCipherAttachment method to CipherService * [PM-22750] Refactor download attachment component to use signals * [PM-22750] Better download url handling * [PM-22750] Cleanup upgradeOldCipherAttachments method * [PM-22750] Refactor cipher-attachments.component to use Signals and OnPush * [PM-22750] Use the correct legacy decryption key for attachments without their own content encryption key * [PM-22750] Add fix attachment button back to attachments component * [PM-22750] Fix newly added output signals * [PM-22750] Fix failing test due to signal refactor * [PM-22750] Update copy
This commit is contained in:
@@ -161,6 +161,17 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
userId: UserId,
|
||||
admin?: boolean,
|
||||
): Promise<Cipher>;
|
||||
/**
|
||||
* Upgrade all old attachments for a cipher by downloading, decrypting, re-uploading with new key, and deleting old.
|
||||
* @param cipher - The cipher with old attachments to upgrade
|
||||
* @param userId - The user ID
|
||||
* @param attachmentId - If provided, only upgrade the attachment with this ID
|
||||
*/
|
||||
abstract upgradeOldCipherAttachments(
|
||||
cipher: CipherView,
|
||||
userId: UserId,
|
||||
attachmentId?: string,
|
||||
): Promise<CipherView>;
|
||||
/**
|
||||
* Save the collections for a cipher with the server
|
||||
*
|
||||
@@ -274,7 +285,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
response: Response,
|
||||
userId: UserId,
|
||||
useLegacyDecryption?: boolean,
|
||||
): Promise<Uint8Array | null>;
|
||||
): Promise<Uint8Array>;
|
||||
|
||||
/**
|
||||
* Decrypts the full `CipherView` for a given `CipherViewLike`.
|
||||
|
||||
@@ -1656,12 +1656,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await firstValueFrom(
|
||||
this.keyService.orgKeys$(userId).pipe(
|
||||
filterOutNullish(),
|
||||
map((orgKeys) => orgKeys[cipherDomain.organizationId as OrganizationId] as OrgKey),
|
||||
),
|
||||
);
|
||||
: cipherDomain.organizationId
|
||||
? await firstValueFrom(
|
||||
this.keyService.orgKeys$(userId).pipe(
|
||||
filterOutNullish(),
|
||||
map((orgKeys) => orgKeys[cipherDomain.organizationId as OrganizationId] as OrgKey),
|
||||
),
|
||||
)
|
||||
: await firstValueFrom(this.keyService.userKey$(userId).pipe(filterOutNullish()));
|
||||
return await this.encryptService.decryptFileData(encBuf, key);
|
||||
}
|
||||
|
||||
@@ -1829,6 +1831,95 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade all old attachments for a cipher by downloading, decrypting, re-uploading with new key, and deleting old.
|
||||
* @param cipher
|
||||
* @param userId
|
||||
* @param attachmentId Optional specific attachment ID to upgrade. If not provided, all old attachments will be upgraded.
|
||||
*/
|
||||
async upgradeOldCipherAttachments(
|
||||
cipher: CipherView,
|
||||
userId: UserId,
|
||||
attachmentId?: string,
|
||||
): Promise<CipherView> {
|
||||
if (!cipher.hasOldAttachments) {
|
||||
return cipher;
|
||||
}
|
||||
|
||||
let cipherDomain = await this.get(cipher.id, userId);
|
||||
|
||||
for (const attachmentView of cipher.attachments) {
|
||||
if (
|
||||
attachmentView.key != null ||
|
||||
(attachmentId != null && attachmentView.id !== attachmentId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Get download URL
|
||||
const downloadUrl = await this.getAttachmentDownloadUrl(cipher.id, attachmentView);
|
||||
|
||||
// 2. Download attachment data
|
||||
const dataResponse = await this.apiService.nativeFetch(
|
||||
new Request(downloadUrl, { cache: "no-store" }),
|
||||
);
|
||||
|
||||
if (dataResponse.status !== 200) {
|
||||
throw new Error(`Failed to download attachment. Status: ${dataResponse.status}`);
|
||||
}
|
||||
|
||||
// 3. Decrypt the attachment
|
||||
const decryptedBuffer = await this.getDecryptedAttachmentBuffer(
|
||||
cipher.id as CipherId,
|
||||
attachmentView,
|
||||
dataResponse,
|
||||
userId,
|
||||
);
|
||||
|
||||
// 4. Re-upload with attachment key
|
||||
cipherDomain = await this.saveAttachmentRawWithServer(
|
||||
cipherDomain,
|
||||
attachmentView.fileName,
|
||||
decryptedBuffer,
|
||||
userId,
|
||||
);
|
||||
|
||||
// 5. Delete the old attachment
|
||||
const cipherData = await this.deleteAttachmentWithServer(
|
||||
cipher.id,
|
||||
attachmentView.id,
|
||||
userId,
|
||||
);
|
||||
cipherDomain = new Cipher(cipherData);
|
||||
} catch (e) {
|
||||
this.logService.error(`Failed to upgrade attachment ${attachmentView.id}`, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.decrypt(cipherDomain, userId);
|
||||
}
|
||||
|
||||
private async getAttachmentDownloadUrl(
|
||||
cipherId: string,
|
||||
attachmentView: AttachmentView,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const attachmentResponse = await this.apiService.getAttachmentData(
|
||||
cipherId,
|
||||
attachmentView.id,
|
||||
);
|
||||
return attachmentResponse.url;
|
||||
} catch (e) {
|
||||
// Fall back to the attachment's stored URL
|
||||
if (e instanceof ErrorResponse && e.statusCode === 404 && attachmentView.url) {
|
||||
return attachmentView.url;
|
||||
}
|
||||
throw new Error(`Failed to get download URL for attachment ${attachmentView.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async encryptObjProperty<V extends View, D extends Domain>(
|
||||
model: V,
|
||||
obj: D,
|
||||
|
||||
Reference in New Issue
Block a user