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 | 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 5x 5x 5x 5x 5x 5x 5x 5x 1x 4x 4x 4x 4x 4x 4x 4x 4x 121x 121x 30x 91x 15x 76x 1x 75x 121x 15x 15x 15x 15x 19x 19x 19x 1x 1x 1x 18x 18x 18x 18x 6x 5x 5x 5x 1x 18x 18x 18x 18x 15x | "use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolvePayloadKey = resolvePayloadKey;
exports.resolveBlobKey = resolveBlobKey;
exports.materializeRecoveredCredentials = materializeRecoveredCredentials;
const PassmanCrypto_1 = require("../PassmanCrypto");
const payloadCrypto_1 = require("./payloadCrypto");
const fileBlobScan_1 = require("./fileBlobScan");
const fileRefs_1 = require("./fileRefs");
/**
* Resolves the plaintext key that decrypts a credential's payload fields, per credential kind.
*
* `hit.workingKeyId` is a {@link VaultKeyCandidateInterface} id, but the candidate password is only
* the payload key for `owned` / `shared_key_mismatch`. For `shared_by_us` it identifies the wrapper
* candidate (the payload is keyed by the decrypted `shared_key`), and for `shared_with_us` it
* identifies the candidate that decrypts the ACL key (the payload is keyed by that decrypted ACL key).
*/
function resolvePayloadKey(internal, candidates) {
if (internal.hit.kind === 'shared_by_us') {
return internal.sharedKeyPlain;
}
if (internal.hit.kind === 'shared_with_us') {
return internal.aclKeyPlain;
}
const keyId = internal.hit.workingKeyId;
if (keyId === undefined) {
return undefined;
}
return candidates.find((candidate) => candidate.id === keyId)?.password;
}
/**
* Resolves the plaintext key that decrypts a single file blob.
*
* `fileHit.workingKeyId` is deliberately left `undefined` when the blob decrypts with the credential's
* `shared_key` or ACL key plaintext rather than a vault candidate (see `fileBlobScan.isSentinelProbeId`),
* so those two cases fall through to the internal's resolved sentinel keys.
*/
function resolveBlobKey(internal, fileHit, candidates) {
Eif (fileHit.workingKeyId !== undefined) {
return (0, payloadCrypto_1.requireCandidate)(candidates, fileHit.workingKeyId, 'file blob').password;
}
if (internal.hit.kind === 'shared_by_us') {
return internal.sharedKeyPlain;
}
if (internal.hit.kind === 'shared_with_us') {
return internal.aclKeyPlain;
}
return undefined;
}
async function materializeFile(credential, internal, fileHit, ref, candidates, vault) {
const filename = ref?.file.filename ?? fileHit.filename;
const source = ref?.source ?? fileHit.source;
const mimetype = ref?.file.mimetype ?? '';
const size = ref?.file.size ?? 0;
const guid = ref?.file.guid ?? fileHit.guid;
const created = ref?.file.created ?? new Date().getTime();
const file_id = ref?.file.file_id ?? fileHit.file_id;
if (fileHit.status === 'broken') {
return { source, filename, mimetype, size, guid, created, file_id, broken: true };
}
const plaintextDecryptionKey = resolveBlobKey(internal, fileHit, candidates);
Iif (plaintextDecryptionKey === undefined) {
return { source, filename, mimetype, size, guid, created, file_id, broken: true };
}
const file = ref?.file ?? {
filename,
size: size,
mimetype: mimetype,
data: '',
file_id: fileHit.file_id,
guid: fileHit.guid
};
try {
const downloaded = await (0, fileBlobScan_1.downloadRawFileBlob)(credential, internal.hit.kind, file, vault);
Iif (!downloaded || typeof downloaded.file_data !== 'string') {
throw new Error('Failed to download file blob: ' + filename);
}
const plaintext = PassmanCrypto_1.PassmanCrypto.decryptString(downloaded.file_data, plaintextDecryptionKey);
return { source, filename, mimetype, size, guid, created, file_id, plaintext, broken: false };
}
catch (e) {
return { source, filename, mimetype, size, guid, created, file_id, broken: true };
}
}
function isSelected(internal, options) {
const hit = internal.hit;
if (hit.status === 'skipped' || hit.status === 'broken') {
return false;
}
// shared-with-us payloads cannot be repaired in place; copying them elsewhere is opt-in only
if (hit.kind === 'shared_with_us') {
return options.credentialGuids !== undefined && options.credentialGuids.includes(hit.guid);
}
if (hit.status === 'ok' && !options.includeHealthy) {
return false;
}
return options.credentialGuids === undefined || options.credentialGuids.includes(hit.guid);
}
/**
* Resolves plaintext credential + file data for the copy destinations (new vault / Passman JSON).
* Read-only: only ever issues GET file-blob downloads, never mutates `vault` or its credentials!
*/
async function materializeRecoveredCredentials(vault, internals, candidates, options, onProgress) {
const selected = internals.filter((internal) => isSelected(internal, options));
const progress = {
doneSteps: 0,
totalSteps: selected.length,
messages: [],
stage: 'materialize'
};
onProgress?.(progress);
const materialized = [];
for (const internal of selected) {
const hit = internal.hit;
const credential = vault.getCredentialByGuid(hit.guid);
if (!credential || credential.hidden === true) {
progress.doneSteps++;
onProgress?.(progress);
continue;
}
const refs = (0, fileBlobScan_1.resolveFileRefs)(credential.getEncrypted(), internal);
const files = [];
const brokenFileNames = [];
for (const fileHit of hit.fileHits) {
const ref = refs.find((candidateRef) => (0, fileRefs_1.fileRefMatchesHit)(candidateRef.file, fileHit));
const file = await materializeFile(credential, internal, fileHit, ref, candidates, vault);
files.push(file);
if (file.broken) {
brokenFileNames.push(file.filename);
}
}
materialized.push({
guid: hit.guid,
label: hit.label,
user_id: credential.user_id,
vault_id: credential.vault_id,
credential_id: credential.credential_id,
kind: hit.kind,
status: hit.status,
// recoveredPreview only ever holds the (candidate-)encrypted payload fields; the plain,
// never-encrypted metadata fields are merged in here from the live credential so the copy
// destinations (new vault / Passman JSON) don't silently drop them. Deliberately excludes
// guid/vault_id/user_id/credential_id: those must come from the destination, not the source.
decrypted: {
...internal.payload.recoveredPreview,
icon: credential.icon,
renew_interval: credential.renew_interval,
expire_time: credential.expire_time,
delete_time: credential.delete_time,
hidden: credential.hidden,
created: credential.created,
changed: credential.changed
},
files,
brokenFields: [...internal.payload.brokenFields],
brokenFileNames
});
progress.doneSteps++;
progress.messages.push(`materialized: ${hit.label}`);
onProgress?.(progress);
}
return materialized;
}
|