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 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 33x 33x 72x 72x 72x 72x 72x 72x 585x 585x 585x 585x 585x 72x 585x 40x 40x 41x 40x 1x 1x 39x 39x 10x 12x 10x 6x 1x 1x 5x 5x 5x 6x 6x 23x 23x 23x 187x 187x 23x 23x 23x 23x 23x 48x 48x 48x 48x 48x 48x 48x 45x 47x 47x 47x 1x 1x 1x 48x 48x 23x 48x 48x 1x | "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VaultRecoveryService = void 0;
const payloadCrypto_1 = require("./payloadCrypto");
const credentialScan_1 = require("./credentialScan");
const fileBlobScan_1 = require("./fileBlobScan");
const payloadRepair_1 = require("./payloadRepair");
const fileBlobRepair_1 = require("./fileBlobRepair");
const materialize_1 = require("./materialize");
const passmanJsonExport_1 = require("./passmanJsonExport");
const newVaultCopy_1 = require("./newVaultCopy");
/**
* Scans a vault for credentials left on the wrong vault key (interrupted vault password change) or with a
* shared_key/vault-key mismatch (Passman <=2.4.4 shared-credential re-encryption bug, see
* https://github.com/nextcloud/passman/issues/737 and https://github.com/nextcloud/passman/pull/768), and repairs
* them individually, allowing partial success (unlike the all-or-nothing {@link ReEncryptionService.changeVaultKey}).
*
* Scanning and the decrypt-side of repair never mutate `vault` or its credentials: candidate keys are only ever
* used with raw {@link PassmanCrypto.decryptString} calls directly on ciphertext, so a wrong candidate never
* triggers `hasUnspecifiedEncryptionError()`/error logging on the live {@link Credential} instances.
*
* Service scope:
* - owned credentials on an old vault key
* - shared-by-us credentials with their shared_key wrapper on an old vault key
* - shared-by-us credentials whose payload is not actually encrypted with that shared key (a client would never
* try the vault key once `shared_key` is set). Fields the shared key does not open are tried with every vault
* candidate. When the wrapper itself decrypts, those fields are put back under the shared key. When it does
* not, `shared_key` is cleared and the recovered fields are re-encrypted as a normal owned credential
* - shared-with-us credentials whose ACL key is on an old vault key
* - the past bug that stored unencrypted `"null"` in `compromised` (an encrypted field); repair persists encrypted `false`
* - file blobs (`files[]` and file-type `custom_fields`) still on an old key after the payload already moved
*
* Not in scope / limitations: revision repair, in-place writes of credentials shared with us (file hits are diagnostic
* only), and vaults where no candidate decrypts anything.
* Credentials shared with us cannot simply have their payload rewritten, so a `"compromised": "null"` sentinel
* on those is not a standalone repair reason and is left in place.
*/
class VaultRecoveryService {
/**
* Read-only diagnosis pass. Never uploads/mutates anything.
* Two stages: credential ciphertext, then HTTP file-blob probes (including payload-`ok` credentials).
*
* Usually not really needed, since it's more efficient to just call repair with dryRun set to true.
*
* @param vault
* @param candidates exactly one candidate should have `preferred: true` (the migration target); falls back to the first candidate otherwise
* @param onProgress called once before the first credential, after every scanned credential, then for the file stage
*/
static async scan(vault, candidates, skipDeleted = true, onProgress) {
const { report } = await this.scanInternal(vault, candidates, skipDeleted, onProgress);
return report;
}
/**
* Same diagnosis pass as {@link scan}, but also returns the per-credential internals
* (resolved shared_key/ACL plaintext, per-field key ids) so repair and materialize can reuse
* them without re-deriving keys that only exist transiently during the scan.
*/
static async scanInternal(vault, candidates, skipDeleted = true, onProgress) {
const preferred = (0, payloadCrypto_1.getPreferredCandidate)(candidates);
const preferredKeyId = preferred?.id;
const progress = {
doneSteps: 0,
totalSteps: vault.credentials.length,
messages: [],
stage: 'credentials'
};
onProgress?.(progress);
const internals = [];
for (const credential of vault.credentials) {
const internal = (0, credentialScan_1.scanCredential)(credential, candidates, preferredKeyId, skipDeleted);
internals.push(internal);
progress.doneSteps++;
progress.messages.push(`${internal.hit.status}: ${internal.hit.label}`);
onProgress?.(progress);
}
await (0, fileBlobScan_1.probeFileBlobs)(vault, internals, candidates, preferred, progress, onProgress);
return { report: (0, credentialScan_1.buildScanReport)(internals.map((internal) => internal.hit)), internals };
}
/**
* Repairs/copies credentials found by an {@link scanInternal} call.
* `destination: 'in_place'` (default) applies each hit's suggested strategy to the original vault.
* `destination: 'new_vault'` / `'passman_json'` materialize the recoverable (+ optionally healthy)
* credentials and copy/export them elsewhere, leaving the original vault untouched.
* Partial success is expected and reported per credential; one failure never aborts the rest.
*/
static async repair(vault, options) {
const { preferredKeyId, candidates, credentialGuids, dryRun, onProgress, onError, includeHealthy, newVaultName } = options;
const destination = options.destination ?? 'in_place';
const preferred = candidates.find((candidate) => candidate.id === preferredKeyId);
if (!preferred) {
onError?.(`Preferred key candidate not found: ${preferredKeyId}`);
return { items: [], succeeded: 0, failed: 0, dryRun, destination };
}
const { report, internals } = await this.scanInternal(vault, candidates, options.skipDeleted ?? true, onProgress);
switch (destination) {
case 'passman_json': {
const materialized = await (0, materialize_1.materializeRecoveredCredentials)(vault, internals, candidates, { credentialGuids, includeHealthy: includeHealthy ?? true }, onProgress);
const items = materialized.map((credential) => ({
guid: credential.guid,
label: credential.label,
action: 'exported',
success: true,
dryRun
}));
return {
items,
succeeded: items.length,
failed: 0,
dryRun,
destination,
json: dryRun ? undefined : (0, passmanJsonExport_1.buildPassmanJson)(materialized),
report
};
}
case 'new_vault': {
if (!newVaultName) {
onError?.('newVaultName is required for destination: new_vault');
return { items: [], succeeded: 0, failed: 0, dryRun, destination };
}
const materialized = await (0, materialize_1.materializeRecoveredCredentials)(vault, internals, candidates, { credentialGuids, includeHealthy: includeHealthy ?? true }, onProgress);
const { items, createdVaultGuid, createdVaultName } = await (0, newVaultCopy_1.copyIntoNewVault)(vault, materialized, newVaultName, preferred, dryRun, onProgress, onError);
return {
items,
succeeded: items.filter((item) => item.success).length,
failed: items.filter((item) => !item.success).length,
dryRun,
destination,
createdVaultGuid,
createdVaultName,
report
};
}
case 'in_place':
default: {
const result = await this.repairInPlace(vault, internals, candidates, preferred, credentialGuids, dryRun, onProgress, onError);
return { ...result, report };
}
}
}
static async repairInPlace(vault, internals, candidates, preferred, credentialGuids, dryRun, onProgress, onError) {
const actionableHits = internals
.map((internal) => internal.hit)
.filter((hit) => (0, payloadRepair_1.isActionableStatus)(hit.status) &&
hit.suggestedStrategy !== undefined &&
(credentialGuids === undefined || credentialGuids.includes(hit.guid)));
const items = [];
const progress = {
doneSteps: 0,
totalSteps: actionableHits.length,
messages: [],
stage: 'repair'
};
progress.messages.push(`Repairing ${actionableHits.length} actionable credentials`);
onProgress?.(progress);
for (const hit of actionableHits) {
const credential = vault.getCredentialByGuid(hit.guid);
const strategy = hit.suggestedStrategy;
Iif (!credential) {
const message = 'Credential not found in vault: ' + hit.guid;
onError?.(message);
items.push({ guid: hit.guid, label: hit.label, strategy, action: 'in_place', success: false, error: message, dryRun });
}
else {
progress.messages.push(`Repairing (${strategy}): ${hit.label}`);
onProgress?.(progress);
try {
if (!(0, payloadRepair_1.shouldSkipCredentialPayloadStrategy)(hit)) {
await (0, payloadRepair_1.applyStrategy)(strategy, vault, credential, hit, candidates, preferred, dryRun);
}
const live = vault.getCredentialByGuid(hit.guid) ?? credential;
await (0, fileBlobRepair_1.repairFileBlobs)(vault, live, hit, candidates, preferred, dryRun);
items.push({ guid: hit.guid, label: hit.label, strategy, action: 'in_place', success: true, dryRun });
}
catch (e) {
const message = e instanceof Error ? e.message : String(e);
onError?.(`${hit.label}: ${message}`);
items.push({ guid: hit.guid, label: hit.label, strategy, action: 'in_place', success: false, error: message, dryRun });
}
}
progress.doneSteps++;
onProgress?.(progress);
}
return {
items,
succeeded: items.filter((item) => item.success).length,
failed: items.filter((item) => !item.success).length,
dryRun,
destination: 'in_place'
};
}
}
exports.VaultRecoveryService = VaultRecoveryService;
|