All files / lib/Service/VaultRecovery payloadRepair.js

92% Statements 92/100
81.25% Branches 39/48
100% Functions 11/11
91.91% Lines 91/99

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  1x 1x 1x 1x 1x 1x 1x 1x   187x             48x 48x 43x   5x 1x   4x           16x 16x 16x               5x 1x               17x 17x 10x   7x 7x 7x 7x 7x 7x 1x   6x               10x 10x 10x     10x 10x 5x   5x 5x 5x 5x 5x   5x 5x 5x     5x               8x 8x 8x 8x     8x 8x     8x 5x   3x 3x 3x 3x 3x 3x 3x 3x     3x                 2x 2x     2x 2x 2x 2x   2x 2x 2x 2x     2x               8x 8x     8x 8x 8x 5x   3x 3x     3x 3x 3x     45x   17x   10x   8x   2x   8x      
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isActionableStatus = isActionableStatus;
exports.shouldSkipCredentialPayloadStrategy = shouldSkipCredentialPayloadStrategy;
exports.applyStrategy = applyStrategy;
const PassmanCrypto_1 = require("../PassmanCrypto");
const ShareService_1 = require("../ShareService");
const payloadCrypto_1 = require("./payloadCrypto");
const types_1 = require("./types");
function isActionableStatus(status) {
    return status === 'needs_rewrap' || status === 'shared_key_mismatch' || status === 'partially_broken';
}
/**
 * Originally-ok payload (classify dropped fieldKeyIds / recoveredPreview) plus files that need a rewrap:
 * skip payload rewrite and only run the file blob phase.
 */
function shouldSkipCredentialPayloadStrategy(hit) {
    const filesNeedRewrap = hit.fileHits.some((fileHit) => fileHit.status === 'needs_rewrap');
    if (!filesNeedRewrap) {
        return false;
    }
    if (hit.status === 'shared_key_mismatch' || hit.status === 'partially_broken') {
        return false;
    }
    return hit.fieldKeyIds === undefined;
}
/**
 * Just the replacement assignment on the given data structures, no calls to the server.
 */
function replaceCredentialInVault(vault, oldCredential, newCredential) {
    const index = vault.credentials.indexOf(oldCredential);
    Eif (index !== -1) {
        vault.credentials[index] = newCredential;
    }
}
/**
 * Encrypts `compromised = false` on `credential` using whatever payload key {@link Credential} would pick
 * (vault key, shared_key, or ACL key). Caller must have already set `overwriteVaultKey` / `shared_key` ciphertext.
 */
function persistEncryptedFalseCompromisedIfUnencryptedNull(credential) {
    if ((0, payloadCrypto_1.isUnencryptedCompromisedANullString)(credential.getEncrypted())) {
        credential.compromised = false;
    }
}
/**
 * Decrypts with the password of `hit.workingKeyId`, re-encrypts like {@link Credential.reEncryptAsClone} would for a
 * healthy owned credential, and (unless dryRun) uploads + swaps the live credential in `vault.credentials`.
 */
async function repairOwned(vault, credential, hit, candidates, preferred, dryRun) {
    const decryptedData = (0, payloadCrypto_1.decryptRecoverablePayload)(credential.getEncrypted(), hit, candidates);
    if (dryRun) {
        return;
    }
    const newCredential = credential.clone();
    newCredential.overwriteVaultKey = preferred.password;
    newCredential.updateData(decryptedData);
    newCredential.skip_revision = true;
    const response = await newCredential.update();
    if (!response) {
        throw new Error('Server rejected the credential update.');
    }
    replaceCredentialInVault(vault, credential, newCredential);
}
/**
 * Only rewraps the shared_key ciphertext from the old to the preferred vault key;
 * the payload (still keyed by the unchanged, decrypted shared_key) is left untouched,
 * except for persisting encrypted `false` when `compromised` is the unencrypted `"null"` sentinel.
 */
async function repairSharedByUsWrapper(vault, credential, hit, candidates, preferred, dryRun) {
    const fromCandidate = (0, payloadCrypto_1.requireCandidate)(candidates, hit.workingKeyId, 'working');
    const wrapperCiphertext = credential.getEncrypted().shared_key;
    Iif (wrapperCiphertext === undefined || wrapperCiphertext === null) {
        throw new Error('Credential has no shared_key wrapper to rewrap.');
    }
    const decryptedSharedKey = PassmanCrypto_1.PassmanCrypto.decryptString(wrapperCiphertext, fromCandidate.password);
    if (dryRun) {
        return;
    }
    const newCredential = credential.clone();
    newCredential.set_share_key = true;
    newCredential.skip_revision = true;
    newCredential.overwriteVaultKey = preferred.password;
    newCredential.getEncrypted().shared_key = PassmanCrypto_1.PassmanCrypto.encryptString(decryptedSharedKey, preferred.password);
    // payload is keyed by shared_key; persist encrypted false under that key, not the vault key
    persistEncryptedFalseCompromisedIfUnencryptedNull(newCredential);
    const response = await newCredential.update();
    Iif (!response) {
        throw new Error('Server rejected the credential update.');
    }
    replaceCredentialInVault(vault, credential, newCredential);
}
/**
 * Restores the shared-by-us model when `shared_key` decrypts but some payload fields were encrypted with a vault
 * key instead: decrypts each recovered field with whichever key actually opens it, rewraps the shared_key to the
 * preferred vault key, and re-encrypts those fields under the shared key again.
 */
async function repairSharedKeyMismatch(vault, credential, hit, candidates, preferred, dryRun) {
    const wrapperCandidate = (0, payloadCrypto_1.requireCandidate)(candidates, hit.wrapperKeyId, 'shared_key wrapper');
    const encrypted = credential.getEncrypted();
    const wrapperCiphertext = encrypted.shared_key;
    Iif (wrapperCiphertext === undefined || wrapperCiphertext === null) {
        throw new Error('Credential has no shared_key wrapper to restore.');
    }
    const decryptedSharedKey = PassmanCrypto_1.PassmanCrypto.decryptString(wrapperCiphertext, wrapperCandidate.password);
    const decryptedData = (0, payloadCrypto_1.decryptRecoverablePayload)(encrypted, hit, candidates, {
        [types_1.SHARED_KEY_PROBE_ID]: decryptedSharedKey
    });
    if (dryRun) {
        return;
    }
    const newCredential = credential.clone();
    newCredential.set_share_key = true;
    newCredential.skip_revision = true;
    newCredential.overwriteVaultKey = preferred.password;
    newCredential.getEncrypted().shared_key = PassmanCrypto_1.PassmanCrypto.encryptString(decryptedSharedKey, preferred.password);
    newCredential.updateData(decryptedData);
    const response = await newCredential.update();
    Iif (!response) {
        throw new Error('Server rejected the credential update.');
    }
    replaceCredentialInVault(vault, credential, newCredential);
}
/**
 * `shared_key` is set, so clients refuse the vault key, but no candidate can open the wrapper.
 * Payload fields that do open with a vault key are re-encrypted under the preferred vault key and
 * `shared_key` is cleared (`unshare_action`) so the credential is owned again.
 * ACL rows are left in place: this recovers the owner's copy, it does not unshare recipients.
 */
async function repairClearUndecryptableSharedKey(vault, credential, hit, candidates, preferred, dryRun) {
    const decryptedData = (0, payloadCrypto_1.decryptRecoverablePayload)(credential.getEncrypted(), hit, candidates);
    Iif (dryRun) {
        return;
    }
    const newCredential = credential.clone();
    newCredential.skip_revision = true;
    newCredential.unshare_action = true;
    newCredential.overwriteVaultKey = preferred.password;
    // clear before updateData, otherwise getFieldEncryptionKey still tries the leftover shared_key
    newCredential.getEncrypted().shared_key = null;
    newCredential.updateData(decryptedData);
    const response = await newCredential.update();
    Iif (!response) {
        throw new Error('Server rejected the credential update.');
    }
    replaceCredentialInVault(vault, credential, newCredential);
}
/**
 * Rewraps the ACL encryptedSharedCredentialEncryptionKey for a credential shared with us, via
 * {@link ShareService.updateSharedKeyForCredentialSharedWithUsByGuid}.
 * Does not touch the owner's credential payload (usually no write access to it).
 */
async function repairAclSharedWithUs(vault, credential, hit, candidates, preferred, dryRun) {
    const fromCandidate = (0, payloadCrypto_1.requireCandidate)(candidates, hit.workingKeyId, 'working');
    Iif (credential.encryptedSharedCredentialEncryptionKey === undefined) {
        throw new Error('Credential is not shared with us; ACL key rewrap is not applicable.');
    }
    const decryptedAclKey = PassmanCrypto_1.PassmanCrypto.decryptString(credential.encryptedSharedCredentialEncryptionKey, fromCandidate.password);
    const reEncryptedAclKey = PassmanCrypto_1.PassmanCrypto.encryptString(decryptedAclKey, preferred.password);
    if (dryRun) {
        return;
    }
    const response = await ShareService_1.ShareService.updateSharedKeyForCredentialSharedWithUsByGuid(reEncryptedAclKey, credential.guid, vault.getServer());
    Iif (!response) {
        throw new Error('Server rejected the ACL shared_key update.');
    }
    credential.encryptedSharedCredentialEncryptionKey = reEncryptedAclKey;
    credential.overwriteVaultKey = undefined;
    credential.clearDecryptedDataCache();
}
async function applyStrategy(strategy, vault, credential, hit, candidates, preferred, dryRun) {
    switch (strategy) {
        case 'rewrap_owned':
            return repairOwned(vault, credential, hit, candidates, preferred, dryRun);
        case 'rewrap_shared_by_us_wrapper':
            return repairSharedByUsWrapper(vault, credential, hit, candidates, preferred, dryRun);
        case 'fix_shared_key_mismatch':
            return repairSharedKeyMismatch(vault, credential, hit, candidates, preferred, dryRun);
        case 'clear_undecryptable_shared_key':
            return repairClearUndecryptableSharedKey(vault, credential, hit, candidates, preferred, dryRun);
        case 'rewrap_acl_shared_with_us':
            return repairAclSharedWithUs(vault, credential, hit, candidates, preferred, dryRun);
    }
}