All files / lib/Service/VaultRecovery fileBlobRepair.js

85% Statements 68/80
67.39% Branches 31/46
100% Functions 9/9
84% Lines 63/75

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 141 142 143  1x 1x 1x 1x 1x 1x 1x   5x               5x     5x 5x     4x 3x   1x 1x 1x           4x 3x 3x 3x     3x 3x 3x   1x 1x 1x     1x 1x                 47x 8x   39x 39x 34x   5x 1x 1x   1x   4x 4x 4x 4x 4x 4x 4x 4x 4x 4x     4x           4x 4x     4x                 4x 4x               4x 4x             4x 4x 4x 4x               4x 4x 4x       4x        
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.repairFileBlobs = repairFileBlobs;
const File_1 = require("../../Model/File");
const PassmanCrypto_1 = require("../PassmanCrypto");
const payloadCrypto_1 = require("./payloadCrypto");
const fileBlobScan_1 = require("./fileBlobScan");
const fileRefs_1 = require("./fileRefs");
async function decryptFileBlobWithWorkingKey(credential, hit, fileHit, candidates, vault) {
    const downloaded = await (0, fileBlobScan_1.downloadRawFileBlob)(credential, hit.kind, {
        filename: fileHit.filename,
        size: 0,
        mimetype: '',
        data: '',
        file_id: fileHit.file_id,
        guid: fileHit.guid
    }, vault);
    Iif (!downloaded || typeof downloaded.file_data !== 'string') {
        throw new Error('Failed to download file blob: ' + fileHit.filename);
    }
    const working = (0, payloadCrypto_1.requireCandidate)(candidates, fileHit.workingKeyId, 'file blob');
    return PassmanCrypto_1.PassmanCrypto.decryptString(downloaded.file_data, working.password);
}
function findLiveFileRef(credential, fileHit) {
    if (fileHit.source === 'files') {
        return credential.files.find((file) => (0, fileRefs_1.fileRefMatchesHit)(file, fileHit));
    }
    for (const field of credential.custom_fields) {
        Eif (field.field_type === 'file' && (0, fileRefs_1.isFileRef)(field.value) && (0, fileRefs_1.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) => (0, fileRefs_1.fileRefMatchesHit)(file, fileHit));
        Iif (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' && (0, fileRefs_1.isFileRef)(field.value) && (0, fileRefs_1.fileRefMatchesHit)(field.value, fileHit));
    Iif (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
 */
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);
            Iif (!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);
            Iif (!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}`);
        }
    }
    Eif (refsChanged) {
        credential.skip_revision = true;
        const response = await credential.update();
        Iif (!response) {
            // roll-back uploaded files on failure to not leave a mess
            for (const uploaded of newFilesToDeleteOnFailure) {
                await File_1.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) {
            Eif (oldFile.file_id !== undefined) {
                await File_1.File.deleteFile(oldFile, vault.getServer());
            }
        }
    }
    Iif (errors.length > 0) {
        throw new Error('File rewrap incomplete: ' + errors.join('; '));
    }
}