All files / lib/Service/VaultRecovery fileBlobScan.js

86.99% Statements 107/123
85.52% Branches 65/76
94.44% Functions 17/18
87.06% Lines 101/116

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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197  1x 1x 1x 1x 1x 1x 1x 1x 1x   920x 920x                                 460x 460x 460x 460x     122x 20x   102x 93x   9x 3x   6x 6x 6x         122x 122x 122x 304x     304x 304x   122x 40x   122x 20x   122x 244x   122x     20x 20x 14x   20x 2x   18x   2x   20x 2x   20x     20x     33x 33x 20x     13x       30x 30x 2x     2x   28x     28x             21x     21x             21x 21x 1x   20x 44x 44x 20x 20x 9x             11x 13x 11x                   72x 72x 585x 143x 143x   442x 442x       442x 442x   442x 72x 52x 320x   52x     20x 20x 20x 20x 20x 122x 122x 122x 21x 21x 21x 21x 21x   122x 122x      
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveFileRefs = resolveFileRefs;
exports.downloadRawFileBlob = downloadRawFileBlob;
exports.probeFileBlobs = probeFileBlobs;
const File_1 = require("../../Model/File");
const PassmanCrypto_1 = require("../PassmanCrypto");
const payloadCrypto_1 = require("./payloadCrypto");
const fileRefs_1 = require("./fileRefs");
const types_1 = require("./types");
function fillMissingFileFieldFromKey(encrypted, preview, field, internal) {
    Eif (internal.payload.fieldKeyIds[field] !== undefined) {
        return;
    }
    const extraKeys = [internal.sharedKeyPlain, internal.aclKeyPlain].filter((key) => !!key);
    for (const key of extraKeys) {
        try {
            const decrypted = (0, payloadCrypto_1.rawDecryptJsonField)(encrypted, field, key);
            if (Array.isArray(decrypted)) {
                preview[field] = decrypted;
                return;
            }
        }
        catch (e) {
            // try next extra key
        }
    }
}
function resolveFileRefs(encrypted, internal) {
    const preview = { ...internal.payload.recoveredPreview };
    fillMissingFileFieldFromKey(encrypted, preview, 'files', internal);
    fillMissingFileFieldFromKey(encrypted, preview, 'custom_fields', internal);
    return (0, fileRefs_1.collectFileRefs)(preview);
}
function promoteHitIfFilesNeedRewrap(hit, preferredKeyId) {
    if (hit.kind === 'shared_with_us') {
        return;
    }
    if (!hit.fileHits.some((fileHit) => fileHit.status === 'needs_rewrap')) {
        return;
    }
    if (hit.status !== 'ok') {
        return;
    }
    hit.status = 'needs_rewrap';
    hit.suggestedStrategy = hit.kind === 'owned' ? 'rewrap_owned' : 'rewrap_shared_by_us_wrapper';
    Iif (hit.workingKeyId === undefined) {
        hit.workingKeyId = preferredKeyId;
    }
}
function collectBlobKeyProbes(internal, candidates, preferred) {
    const probes = [];
    const seenIds = new Set();
    const add = (id, key) => {
        Iif (!key || seenIds.has(id)) {
            return;
        }
        seenIds.add(id);
        probes.push({ id, key });
    };
    if (internal.hit.kind === 'shared_by_us' && internal.sharedKeyPlain) {
        add(types_1.SHARED_KEY_PROBE_ID, internal.sharedKeyPlain);
    }
    if (internal.hit.kind === 'shared_with_us' && internal.aclKeyPlain) {
        add(types_1.ACL_KEY_PROBE_ID, internal.aclKeyPlain);
    }
    for (const candidate of (0, payloadCrypto_1.orderPreferredFirst)(candidates, preferred?.id)) {
        add(candidate.id, candidate.password);
    }
    return probes;
}
function destinationProbeIds(kind, preferredKeyId, probes) {
    const ids = new Set();
    if (kind === 'owned' && preferredKeyId) {
        ids.add(preferredKeyId);
    }
    if (kind === 'shared_by_us' && probes.some((probe) => probe.id === types_1.SHARED_KEY_PROBE_ID)) {
        ids.add(types_1.SHARED_KEY_PROBE_ID);
    }
    else if (kind === 'shared_by_us' && preferredKeyId) {
        // shared_key did not yield a payload key. Repair clears it and the credential becomes owned.
        ids.add(preferredKeyId);
    }
    if (kind === 'shared_with_us' && probes.some((probe) => probe.id === types_1.ACL_KEY_PROBE_ID)) {
        ids.add(types_1.ACL_KEY_PROBE_ID);
    }
    return ids;
}
function isSentinelProbeId(id) {
    return id === types_1.SHARED_KEY_PROBE_ID || id === types_1.ACL_KEY_PROBE_ID;
}
function blobDecryptsWith(fileData, key) {
    try {
        PassmanCrypto_1.PassmanCrypto.decryptString(fileData, key);
        return true;
    }
    catch (e) {
        return false;
    }
}
async function downloadRawFileBlob(credential, kind, file, vault) {
    try {
        if (kind === 'shared_with_us') {
            Iif (!file.guid) {
                return;
            }
            return await File_1.File.downloadSharedFile(credential, file, vault.getServer());
        }
        Iif (file.file_id === undefined || file.file_id === null) {
            return;
        }
        return await File_1.File.downloadFile(file, vault.getServer());
    }
    catch (e) {
        return;
    }
}
async function probeOneFileBlob(credential, kind, ref, probes, vault, preferredKeyId) {
    const filename = typeof ref.file.filename === 'string' && ref.file.filename.length > 0
        ? ref.file.filename
        : '(unnamed)';
    const base = {
        file_id: ref.file.file_id,
        guid: ref.file.guid,
        filename,
        source: ref.source,
        status: 'broken'
    };
    const downloaded = await downloadRawFileBlob(credential, kind, ref.file, vault);
    if (!downloaded || typeof downloaded.file_data !== 'string') {
        return base;
    }
    const destinationIds = destinationProbeIds(kind, preferredKeyId, probes);
    const destinationProbes = probes.filter((probe) => destinationIds.has(probe.id));
    const otherProbes = probes.filter((probe) => !destinationIds.has(probe.id));
    for (const probe of destinationProbes) {
        if (blobDecryptsWith(downloaded.file_data, probe.key)) {
            return {
                ...base,
                status: 'ok',
                workingKeyId: isSentinelProbeId(probe.id) ? undefined : probe.id
            };
        }
    }
    for (const probe of otherProbes) {
        if (blobDecryptsWith(downloaded.file_data, probe.key)) {
            return {
                ...base,
                status: 'needs_rewrap',
                workingKeyId: isSentinelProbeId(probe.id) ? undefined : probe.id
            };
        }
    }
    return base;
}
async function probeFileBlobs(vault, internals, candidates, preferred, progress, onProgress) {
    const jobs = [];
    for (const internal of internals) {
        if (internal.hit.status === 'skipped' || internal.hit.status === 'broken') {
            internal.hit.fileHits = [];
            continue;
        }
        const credential = vault.getCredentialByGuid(internal.hit.guid);
        Iif (!credential) {
            internal.hit.fileHits = [];
            continue;
        }
        const refs = resolveFileRefs(credential.getEncrypted(), internal);
        jobs.push({ internal, credential, refs });
    }
    const totalFiles = jobs.reduce((sum, job) => sum + job.refs.length, 0);
    if (totalFiles === 0) {
        for (const job of jobs) {
            job.internal.hit.fileHits = [];
        }
        return;
    }
    // now as the pre-filtering and selection of candidates is done, we can do the real work :)
    progress.stage = 'files';
    progress.doneSteps = 0;
    progress.totalSteps = totalFiles;
    onProgress?.(progress);
    for (const job of jobs) {
        const probes = collectBlobKeyProbes(job.internal, candidates, preferred);
        const fileHits = [];
        for (const ref of job.refs) {
            const fileHit = await probeOneFileBlob(job.credential, job.internal.hit.kind, ref, probes, vault, preferred?.id);
            fileHits.push(fileHit);
            progress.doneSteps++;
            progress.messages.push(`file ${fileHit.status}: ${job.internal.hit.label} / ${fileHit.filename}`);
            onProgress?.(progress);
        }
        job.internal.hit.fileHits = fileHits;
        promoteHitIfFilesNeedRewrap(job.internal.hit, preferred?.id);
    }
}