padloc/packages/app/src/lib/1pux-parser.ts

284 lines
7.7 KiB
TypeScript

import { loadAsync } from "jszip";
export type OnePuxItemDetailsLoginField = {
value: string;
id: string;
name: string;
fieldType: "A" | "B" | "C" | "E" | "I" | "N" | "P" | "R" | "S" | "T" | "U";
designation?: "username" | "password";
};
export type OnePuxItemDetailsSection = {
title: string;
name: string;
fields: [
{
title: string;
id: string;
value: {
concealed?: string;
reference?: string;
string?: string;
email?: string;
phone?: string;
url?: string;
totp?: string;
gender?: string;
creditCardType?: string;
creditCardNumber?: string;
monthYear?: number;
date?: number;
};
indexAtSource: number;
guarded: boolean;
multiline: boolean;
dontGenerate: boolean;
inputTraits: {
keyboard: string;
correction: string;
capitalization: string;
};
}
];
};
export type OnePuxItemDetailsPasswordHistory = {
value: string;
time: number;
};
export type OnePuxItemOverviewUrl = {
label: string;
url: string;
};
export type OnePuxItem = {
item?: {
uuid: string;
favIndex: number;
createdAt: number;
updatedAt: number;
trashed: boolean;
categoryUuid: string;
details: {
loginFields: OnePuxItemDetailsLoginField[];
notesPlain?: string;
sections: OnePuxItemDetailsSection[];
passwordHistory: OnePuxItemDetailsPasswordHistory[];
documentAttributes?: {
fileName: string;
documentId: string;
decryptedSize: number;
};
};
overview: {
subtitle: string;
urls?: OnePuxItemOverviewUrl[];
title: string;
url: string;
ps?: number;
pbe?: number;
pgrng?: boolean;
tags?: string[];
};
};
file?: {
attrs: {
uuid: string;
name: string;
type: string;
};
path: string;
};
};
export type OnePuxVault = {
attrs: {
uuid: string;
desc: string;
avatar: string;
name: string;
type: "P" | "E" | "U";
};
items: OnePuxItem[];
};
export type OnePuxAccount = {
attrs: {
accountName: string;
name: string;
avatar: string;
email: string;
uuid: string;
domain: string;
};
vaults: OnePuxVault[];
};
export type OnePuxData = {
accounts: OnePuxAccount[];
};
export type OnePuxAttributes = {
version: number;
description: string;
createdAt: number;
};
export type OnePuxExport = {
attributes: OnePuxAttributes;
data: OnePuxData;
};
export async function parse1PuxFile(fileContents: ArrayBuffer): Promise<OnePuxExport> {
try {
const zip = await loadAsync(fileContents);
const attributesContent = await zip.file("export.attributes")!.async("string");
const attributes = JSON.parse(attributesContent);
const dataContent = await zip.file("export.data")!.async("string");
const data = JSON.parse(dataContent);
return {
attributes,
data,
} as OnePuxExport;
} catch (error) {
console.error("Failed to parse .1pux file");
throw error;
}
}
type RowData = {
name: string;
tags: string;
url: string;
username: string;
password: string;
notes: string;
extraFields: ExtraField[];
};
type ExtraFieldType =
| "username"
| "password"
| "url"
| "email"
| "date"
| "month"
| "credit"
| "phone"
| "totp"
| "text";
type ExtraField = { name: string; value: string; type: ExtraFieldType };
function parseFieldTypeToExtraFieldType(field: OnePuxItemDetailsLoginField): ExtraFieldType {
if (field.designation === "username") {
return "username";
} else if (field.designation === "password") {
return "password";
} else if (field.fieldType === "E") {
return "email";
} else if (field.fieldType === "U") {
return "url";
}
return "text";
}
export function parseToRowData(item: OnePuxItem["item"], defaultTags?: string[]): RowData | undefined {
if (!item) {
return;
}
const rowData: RowData = {
name: item.overview.title,
tags: [...(defaultTags || []), ...(item.overview.tags || [])].join(","),
url: item.overview.url || "",
username: "",
password: "",
notes: item.details.notesPlain || "",
extraFields: [],
};
// Skip documents
if (item.details.documentAttributes && item.details.loginFields.length === 0) {
return;
}
// Extract username, password, and some extraFields
item.details.loginFields.forEach((field) => {
if (field.designation === "username") {
rowData.username = field.value;
} else if (field.designation === "password") {
rowData.password = field.value;
} else if (
field.fieldType === "I" ||
field.fieldType === "C" ||
field.id.includes(";opid=__") ||
field.value === ""
) {
// Skip these noisy form-fields
return;
} else {
rowData.extraFields.push({
name: field.name || field.id,
value: field.value,
type: parseFieldTypeToExtraFieldType(field),
});
}
});
// Extract some more extraFields
item.details.sections.forEach((section) => {
section.fields.forEach((field) => {
let value = "";
let type: ExtraFieldType = "text";
if (field.value.concealed) {
value = field.value.concealed || "";
} else if (field.value.reference) {
value = field.value.reference || "";
} else if (field.value.string) {
value = field.value.string || "";
} else if (field.value.email) {
value = field.value.email || "";
type = "email";
} else if (field.value.phone) {
value = field.value.phone || "";
type = "phone";
} else if (field.value.url) {
value = field.value.url || "";
type = "url";
} else if (field.value.totp) {
value = field.value.totp || "";
type = "totp";
} else if (field.value.gender) {
value = field.value.gender || "";
} else if (field.value.creditCardType) {
value = field.value.creditCardType || "";
} else if (field.value.creditCardNumber) {
value = field.value.creditCardNumber || "";
type = "credit";
} else if (field.value.monthYear) {
value = (field.value.monthYear && field.value.monthYear.toString()) || "";
type = "month";
} else if (field.value.date) {
value = (field.value.date && field.value.date.toString()) || "";
type = "date";
} else {
// Default, so no data is lost when something new comes up
value = JSON.stringify(field.value);
}
rowData.extraFields.push({
name: field.title || field.id,
value,
type,
});
});
});
return rowData;
}