All files / lib/esm/Service/VaultRecovery passmanJsonExport.js

0% Statements 0/30
0% Branches 0/52
0% Functions 0/6
0% Lines 0/28

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                                                                                                                                                                                   
/**
 * `undefined` when the blob or reference is broken (matches importer-passmanjson.js dropping/failing that entry).
 */
function toExportJsonFile(file) {
    if (file.broken || file.plaintext === undefined || file.file_id === undefined || file.guid === undefined) {
        return undefined;
    }
    return {
        file_id: file.file_id,
        filename: file.filename,
        guid: file.guid,
        size: file.size ?? file.plaintext.length,
        file_data: file.plaintext,
        created: file.created ?? new Date().getTime(),
        mimetype: file.mimetype ?? '',
    };
}
/**
 * Splits `materialized.files` back into a `files[]` array and file-type `custom_fields[].value` payloads,
 * matching positions 1:1 against `decrypted.custom_fields` (both derive from the same, unreordered
 * `collectFileRefs` traversal). Broken blobs are dropped rather than exported half-broken.
 */
function buildFilesAndCustomFields(materialized) {
    const filesMaterialized = materialized.files.filter((file) => file.source === 'files');
    const customFieldMaterialized = materialized.files.filter((file) => file.source === 'custom_field');
    const files = [];
    for (const materializedFile of filesMaterialized) {
        const built = toExportJsonFile(materializedFile);
        if (built) {
            files.push(built);
        }
    }
    const decryptedCustomFields = materialized.decrypted.custom_fields ?? [];
    const custom_fields = [];
    let fileFieldCursor = 0;
    for (const field of decryptedCustomFields) {
        if (field.field_type !== 'file') {
            custom_fields.push(field);
            continue;
        }
        const materializedFile = customFieldMaterialized[fileFieldCursor];
        fileFieldCursor++;
        const built = materializedFile ? toExportJsonFile(materializedFile) : undefined;
        if (built) {
            custom_fields.push({ ...field, value: built });
        }
        // drop broken file custom fields entirely, same policy as files[]
    }
    return { files, custom_fields };
}
/**
 * Builds the classic top-level-array Passman JSON export format, matching what
 * `importer-passmanjson.js` actually reads: no `vault_id` / `vault_key` / `shared_key` / `guid` / `acl`,
 * files inlined as `file_data` (re-uploaded and re-encrypted by the importer on read).
 *
 * Known lossy, by design: the importer reads `item.otp.qrCode`, while the decrypted `otp` carries
 * `qr_uri.image`. `secret` / `type` / `issuer` / `label` import fine; the QR image does not. The classic
 * export has the same gap, so the decrypted shape is kept as-is rather than inventing a `qrCode` field.
 */
export function buildPassmanJson(materialized) {
    const entries = materialized.map((credential) => {
        const { files, custom_fields } = buildFilesAndCustomFields(credential);
        const decrypted = credential.decrypted;
        return {
            user_id: credential.user_id,
            vault_id: credential.vault_id,
            credential_id: credential.credential_id,
            label: credential.label,
            username: decrypted.username ?? null,
            password: decrypted.password ?? null,
            email: decrypted.email ?? null,
            url: decrypted.url ?? null,
            description: decrypted.description ?? null,
            tags: decrypted.tags ?? null,
            custom_fields,
            otp: decrypted.otp ?? {},
            compromised: decrypted.compromised ?? null,
            files,
            created: decrypted.created ?? null,
            changed: decrypted.changed ?? null,
            expire_time: decrypted.expire_time ?? null,
            delete_time: decrypted.delete_time ?? null,
            renew_interval: decrypted.renew_interval ?? null,
            hidden: decrypted.hidden ?? false,
            icon: decrypted.icon ?? null,
        };
    });
    return JSON.stringify(entries);
}