All files / lib/Service/VaultRecovery payloadCrypto.js

89.1% Statements 90/101
79.54% Branches 35/44
100% Functions 18/18
88.65% Lines 86/97

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  1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   72x     1272x 1272x 636x     555x 288x     288x     514x 5140x 5140x       5140x               1439x     27x 1x       8996x     8996x 8996x     8996x     8996x     1099x 1099x 1099x   1099x     514x 5x 5x   514x               291x 291x 2907x 2907x 5037x   5037x 2115x 2115x 2115x           2907x 792x     291x           72x 72x 719x   719x 719x           72x               151x 151x 1509x 1509x 1469x   1469x 754x 754x           1509x 755x 1510x   1510x 752x 752x 752x             1509x 3x     151x                   27x 27x 27x 27x 261x 261x   261x   27x 27x                    
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPreferredCandidate = getPreferredCandidate;
exports.orderPreferredFirst = orderPreferredFirst;
exports.requireCandidate = requireCandidate;
exports.getPresentPayloadFields = getPresentPayloadFields;
exports.isUnencryptedCompromisedANullString = isUnencryptedCompromisedANullString;
exports.rawDecryptJsonField = rawDecryptJsonField;
exports.emptyPayloadResult = emptyPayloadResult;
exports.decryptPayloadPerCandidate = decryptPayloadPerCandidate;
exports.decryptPayloadWithKey = decryptPayloadWithKey;
exports.decryptPayloadWithKeyThenCandidates = decryptPayloadWithKeyThenCandidates;
exports.decryptRecoverablePayload = decryptRecoverablePayload;
const PassmanCrypto_1 = require("../PassmanCrypto");
const types_1 = require("./types");
function getPreferredCandidate(candidates) {
    return candidates.find((candidate) => candidate.preferred) ?? candidates[0];
}
function orderPreferredFirst(candidates, preferredKeyId) {
    const preferred = candidates.filter((candidate) => candidate.id === preferredKeyId);
    const rest = candidates.filter((candidate) => candidate.id !== preferredKeyId);
    return [...preferred, ...rest];
}
function requireCandidate(candidates, id, description) {
    const candidate = id !== undefined ? candidates.find((c) => c.id === id) : undefined;
    Iif (!candidate) {
        throw new Error(`No ${description} key candidate found (id: ${id}).`);
    }
    return candidate;
}
function getPresentPayloadFields(encrypted) {
    return types_1.PAYLOAD_FIELDS.filter((field) => {
        const value = encrypted[field];
        Iif (value === undefined || value === null) {
            return false;
        }
        // unencrypted `"null"` sentinel is handled separately; do not probe it as ciphertext
        return !(field === 'compromised' && value === 'null');
    });
}
/**
 * Past bug: `compromised` was stored as the unencrypted sentinel string `"null"` (same shape as a missing `shared_key`) instead of ciphertext.
 * Decrypt-side callers treat this as `false`; repair persists encrypted `false`.
 */
function isUnencryptedCompromisedANullString(encrypted) {
    return encrypted.compromised === 'null';
}
function applyUnencryptedNullCompromisedDefault(encrypted, decryptedData) {
    if (isUnencryptedCompromisedANullString(encrypted)) {
        decryptedData.compromised = false;
    }
}
function rawDecryptJsonField(encrypted, field, key) {
    Iif (field === 'compromised' && isUnencryptedCompromisedANullString(encrypted)) {
        return false;
    }
    const ciphertext = encrypted[field];
    Iif (ciphertext === undefined || ciphertext === null) {
        return null;
    }
    Iif (typeof ciphertext !== 'string') {
        throw new Error('Ciphertext is not a string');
    }
    return JSON.parse(PassmanCrypto_1.PassmanCrypto.decryptString(ciphertext, key));
}
function emptyPayloadResult(label) {
    const recoveredPreview = {};
    Eif (label !== undefined) {
        recoveredPreview.label = label;
    }
    return { fieldKeyIds: {}, recoveredPreview, brokenFields: [], coercedCompromised: false };
}
function finishPayloadDecrypt(encrypted, result) {
    if (isUnencryptedCompromisedANullString(encrypted)) {
        result.recoveredPreview.compromised = false;
        result.coercedCompromised = true;
    }
    return result;
}
/**
 * Tries each present payload field against the given candidates in order.
 * Mixed keys are allowed: a field keeps the first candidate that decrypts it.
 * Fields that decrypt with none are listed in `brokenFields`.
 */
function decryptPayloadPerCandidate(encrypted, candidates, label) {
    const result = emptyPayloadResult(label);
    for (const field of getPresentPayloadFields(encrypted)) {
        let decrypted = false;
        for (const candidate of candidates) {
            try {
                // @ts-ignore transitively gated by PAYLOAD_FIELDS / ENCRYPTED_FIELDS
                result.recoveredPreview[field] = rawDecryptJsonField(encrypted, field, candidate.password);
                result.fieldKeyIds[field] = candidate.id;
                decrypted = true;
                break;
            }
            catch (e) {
                // try next candidate
            }
        }
        if (!decrypted) {
            result.brokenFields.push(field);
        }
    }
    return finishPayloadDecrypt(encrypted, result);
}
/**
 * Decrypts present payload fields with a single payload key (shared_key plaintext or ACL key).
 */
function decryptPayloadWithKey(encrypted, payloadKey, payloadKeyId, label) {
    const result = emptyPayloadResult(label);
    for (const field of getPresentPayloadFields(encrypted)) {
        try {
            // @ts-ignore gated by PAYLOAD_FIELDS / ENCRYPTED_FIELDS
            result.recoveredPreview[field] = rawDecryptJsonField(encrypted, field, payloadKey);
            result.fieldKeyIds[field] = payloadKeyId;
        }
        catch (e) {
            result.brokenFields.push(field);
        }
    }
    return finishPayloadDecrypt(encrypted, result);
}
/**
 * Decrypts each present payload field with `payloadKey` first (the key a normal client would use),
 * then with vault candidates for anything that key does not open.
 * `payloadKeyId` is stored on fields the first key opened (for example `__shared_key__`).
 */
function decryptPayloadWithKeyThenCandidates(encrypted, payloadKey, payloadKeyId, candidates, label) {
    const result = emptyPayloadResult(label);
    for (const field of getPresentPayloadFields(encrypted)) {
        let decrypted = false;
        if (payloadKey !== undefined && payloadKey !== '') {
            try {
                // @ts-ignore gated by PAYLOAD_FIELDS / ENCRYPTED_FIELDS
                result.recoveredPreview[field] = rawDecryptJsonField(encrypted, field, payloadKey);
                result.fieldKeyIds[field] = payloadKeyId;
                decrypted = true;
            }
            catch (e) {
                // field is not under the client-preferred key; try vault candidates below
            }
        }
        if (!decrypted) {
            for (const candidate of candidates) {
                try {
                    // @ts-ignore transitively gated by PAYLOAD_FIELDS / ENCRYPTED_FIELDS
                    result.recoveredPreview[field] = rawDecryptJsonField(encrypted, field, candidate.password);
                    result.fieldKeyIds[field] = candidate.id;
                    decrypted = true;
                    break;
                }
                catch (e) {
                    // try next candidate
                }
            }
        }
        if (!decrypted) {
            result.brokenFields.push(field);
        }
    }
    return finishPayloadDecrypt(encrypted, result);
}
/**
 * Decrypts recoverable payload fields for repair.
 *
 * Uses {@link CredentialRecoveryHitInterface.fieldKeyIds} when present (partial / mixed-key);
 * otherwise decrypts every payload field with `hit.workingKeyId` and throws on the first failure.
 * `extraKeys` maps synthetic ids such as `__shared_key__` to the plaintext those fields were encrypted with.
 */
function decryptRecoverablePayload(encrypted, hit, candidates, extraKeys) {
    const decryptedData = {};
    const fieldIds = hit.fieldKeyIds;
    Eif (fieldIds !== undefined && Object.keys(fieldIds).length > 0) {
        for (const field of Object.keys(fieldIds)) {
            const keyId = fieldIds[field];
            const key = extraKeys?.[keyId] ?? requireCandidate(candidates, keyId, 'field').password;
            // @ts-ignore gated by PAYLOAD_FIELDS
            decryptedData[field] = rawDecryptJsonField(encrypted, field, key);
        }
        applyUnencryptedNullCompromisedDefault(encrypted, decryptedData);
        return decryptedData;
    }
    const fromCandidate = requireCandidate(candidates, hit.workingKeyId, 'working');
    for (const field of types_1.PAYLOAD_FIELDS) {
        // @ts-ignore gated by PAYLOAD_FIELDS
        decryptedData[field] = rawDecryptJsonField(encrypted, field, fromCandidate.password);
    }
    applyUnencryptedNullCompromisedDefault(encrypted, decryptedData);
    return decryptedData;
}