Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | import { File } from "../../Model/File";
import { PassmanCrypto } from "../PassmanCrypto";
import { requireCandidate } from "./payloadCrypto";
import { downloadRawFileBlob } from "./fileBlobScan";
import { fileRefMatchesHit, isFileRef } from "./fileRefs";
async function decryptFileBlobWithWorkingKey(credential, hit, fileHit, candidates, vault) {
const downloaded = await downloadRawFileBlob(credential, hit.kind, {
filename: fileHit.filename,
size: 0,
mimetype: '',
data: '',
file_id: fileHit.file_id,
guid: fileHit.guid
}, vault);
if (!downloaded || typeof downloaded.file_data !== 'string') {
throw new Error('Failed to download file blob: ' + fileHit.filename);
}
const working = requireCandidate(candidates, fileHit.workingKeyId, 'file blob');
return PassmanCrypto.decryptString(downloaded.file_data, working.password);
}
function findLiveFileRef(credential, fileHit) {
if (fileHit.source === 'files') {
return credential.files.find((file) => fileRefMatchesHit(file, fileHit));
}
for (const field of credential.custom_fields) {
if (field.field_type === 'file' && isFileRef(field.value) && fileRefMatchesHit(field.value, fileHit)) {
return field.value;
}
}
return undefined;
}
function replaceLiveFileRef(credential, fileHit, replacement) {
if (fileHit.source === 'files') {
const files = [...credential.files];
const index = files.findIndex((file) => fileRefMatchesHit(file, fileHit));
if (index === -1) {
throw new Error('File reference not found in files[]: ' + fileHit.filename);
}
files[index] = replacement;
credential.files = files;
return;
}
const customFields = credential.custom_fields.map((field) => ({ ...field }));
const index = customFields.findIndex((field) => field.field_type === 'file' && isFileRef(field.value) && fileRefMatchesHit(field.value, fileHit));
if (index === -1) {
throw new Error('File reference not found in custom_fields: ' + fileHit.filename);
}
customFields[index] = { ...customFields[index], value: replacement };
credential.custom_fields = customFields;
}
/**
* Re-uploads blobs that decrypt with a non-destination key. Partial success per file.
*
* - shared_with_us: diagnostic only (no owner writes)
* - dry-run: GET + decrypt, no upload/delete
*/
export async function repairFileBlobs(vault, credential, hit, candidates, preferred, dryRun) {
if (hit.kind === 'shared_with_us') {
return;
}
const needingRewrap = hit.fileHits.filter((fileHit) => fileHit.status === 'needs_rewrap');
if (needingRewrap.length === 0) {
return;
}
if (dryRun) {
for (const fileHit of needingRewrap) {
await decryptFileBlobWithWorkingKey(credential, hit, fileHit, candidates, vault);
}
return;
}
credential.overwriteVaultKey = preferred.password;
const oldFilesToDelete = [];
const newFilesToDeleteOnFailure = [];
const errors = [];
let refsChanged = false;
for (const fileHit of needingRewrap) {
try {
const plaintext = await decryptFileBlobWithWorkingKey(credential, hit, fileHit, candidates, vault);
const original = findLiveFileRef(credential, fileHit);
if (!original) {
throw new Error('File reference not found on credential: ' + fileHit.filename);
}
const plainFile = {
data: plaintext,
filename: original.filename ?? fileHit.filename,
mimetype: original.mimetype,
size: original.size
};
const uploadResponse = await credential.encryptUploadFile(plainFile);
if (!uploadResponse) {
throw new Error('File upload returned no response');
}
const replacement = {
filename: original.filename ?? fileHit.filename,
size: original.size,
mimetype: original.mimetype,
data: '',
file_id: uploadResponse.file_id,
guid: uploadResponse.guid,
created: original.created
};
replaceLiveFileRef(credential, fileHit, replacement);
oldFilesToDelete.push({
filename: original.filename ?? fileHit.filename,
size: original.size,
mimetype: original.mimetype,
data: '',
file_id: original.file_id,
guid: original.guid
});
newFilesToDeleteOnFailure.push(replacement);
refsChanged = true;
}
catch (e) {
const message = e instanceof Error ? e.message : String(e);
errors.push(`${fileHit.filename}: ${message}`);
}
}
if (refsChanged) {
credential.skip_revision = true;
const response = await credential.update();
if (!response) {
// roll-back uploaded files on failure to not leave a mess
for (const uploaded of newFilesToDeleteOnFailure) {
await File.deleteFile(uploaded, vault.getServer());
}
throw new Error('Server rejected the credential update after file re-upload.');
}
// when everything worked well, we can delete the old inconsistently encrypted files
for (const oldFile of oldFilesToDelete) {
if (oldFile.file_id !== undefined) {
await File.deleteFile(oldFile, vault.getServer());
}
}
}
if (errors.length > 0) {
throw new Error('File rewrap incomplete: ' + errors.join('; '));
}
}
|