599 lines
15 KiB
TypeScript
599 lines
15 KiB
TypeScript
import { translate as $l } from "@padloc/locale/src/translate";
|
|
import { base32ToBytes, Serializable, AsSerializable, AsDate } from "./encoding";
|
|
import { totp } from "./otp";
|
|
import { uuid } from "./util";
|
|
import { AccountID } from "./account";
|
|
import { AttachmentInfo } from "./attachment";
|
|
import { openExternalUrl } from "./platform";
|
|
|
|
/** A tag that can be assigned to a [[VaultItem]] */
|
|
export type Tag = string;
|
|
|
|
/** Unique identifier for [[VaultItem]]s */
|
|
export type VaultItemID = string;
|
|
|
|
export enum FieldType {
|
|
Username = "username",
|
|
Password = "password",
|
|
Url = "url",
|
|
Email = "email",
|
|
Date = "date",
|
|
Month = "month",
|
|
Credit = "credit",
|
|
Phone = "phone",
|
|
Pin = "pin",
|
|
Totp = "totp",
|
|
Note = "note",
|
|
Text = "text",
|
|
}
|
|
|
|
/**
|
|
* Field definition containing meta data for a certain field type
|
|
*/
|
|
export interface FieldDef {
|
|
/** content type */
|
|
type: FieldType;
|
|
/** regular expression describing pattern of field contents (used for validation) */
|
|
pattern: RegExp;
|
|
/** regular expression describing pattern of field contents (used for matching) */
|
|
matchPattern: RegExp;
|
|
/** whether the field should be masked when displayed */
|
|
mask: boolean;
|
|
/** whether the field value can have multiple lines */
|
|
multiline: boolean;
|
|
/** icon used for display */
|
|
icon: string;
|
|
/** display name */
|
|
name: string;
|
|
/** display formatting */
|
|
format?: (value: string, masked: boolean) => string;
|
|
/** for values that need to be prepared before being copied / filled */
|
|
transform?: (value: string) => Promise<string>;
|
|
actions?: { icon: string; label: string; action: (value: string) => void }[];
|
|
}
|
|
|
|
/** Available field types and respective meta data (order matters for pattern matching) */
|
|
export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|
[FieldType.Username]: {
|
|
type: FieldType.Username,
|
|
pattern: /.*/,
|
|
matchPattern: /.*/,
|
|
mask: false,
|
|
multiline: false,
|
|
icon: "user",
|
|
get name() {
|
|
return $l("Username");
|
|
},
|
|
},
|
|
[FieldType.Password]: {
|
|
type: FieldType.Password,
|
|
pattern: /.*/,
|
|
matchPattern: /.*/,
|
|
mask: true,
|
|
multiline: true,
|
|
icon: "lock",
|
|
get name() {
|
|
return $l("Password");
|
|
},
|
|
format(value, masked) {
|
|
return masked ? value.replace(/./g, "\u2022") : value;
|
|
},
|
|
},
|
|
[FieldType.Email]: {
|
|
type: FieldType.Email,
|
|
pattern: /(.*)@(.*)/,
|
|
matchPattern: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,8}$/,
|
|
mask: false,
|
|
multiline: false,
|
|
icon: "email",
|
|
get name() {
|
|
return $l("Email Address");
|
|
},
|
|
},
|
|
[FieldType.Url]: {
|
|
type: FieldType.Url,
|
|
pattern: /.*/,
|
|
matchPattern: /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,8}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/i,
|
|
mask: false,
|
|
multiline: false,
|
|
icon: "web",
|
|
get name() {
|
|
return $l("URL");
|
|
},
|
|
actions: [
|
|
{
|
|
icon: "web",
|
|
label: $l("Open"),
|
|
action: (value: string) => openExternalUrl(value.startsWith("http") ? value : `https://${value}`),
|
|
},
|
|
],
|
|
},
|
|
[FieldType.Date]: {
|
|
type: FieldType.Date,
|
|
pattern: /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$/,
|
|
matchPattern: /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$/,
|
|
mask: false,
|
|
multiline: false,
|
|
icon: "date",
|
|
get name() {
|
|
return $l("Date");
|
|
},
|
|
format(value) {
|
|
return new Date(value).toLocaleDateString();
|
|
},
|
|
},
|
|
[FieldType.Month]: {
|
|
type: FieldType.Month,
|
|
pattern: /^\d{4}-(0[1-9]|1[012])$/,
|
|
matchPattern: /^\d{4}-(0[1-9]|1[012])$/,
|
|
mask: false,
|
|
multiline: false,
|
|
icon: "month",
|
|
get name() {
|
|
return $l("Month");
|
|
},
|
|
},
|
|
[FieldType.Credit]: {
|
|
type: FieldType.Credit,
|
|
pattern: /.*/,
|
|
matchPattern: /^\d{16}/,
|
|
mask: true,
|
|
multiline: false,
|
|
icon: "credit",
|
|
get name() {
|
|
return $l("Credit Card Number");
|
|
},
|
|
format(value, masked) {
|
|
const parts = [];
|
|
|
|
for (let i = 0; i < value.length; i += 4) {
|
|
const part = value.slice(i, i + 4);
|
|
parts.push(masked && i < value.length - 4 ? part.replace(/./g, "\u2022") : part);
|
|
}
|
|
|
|
return parts.join(" ");
|
|
},
|
|
},
|
|
[FieldType.Phone]: {
|
|
type: FieldType.Phone,
|
|
pattern: /.*/,
|
|
matchPattern: /\d+/,
|
|
mask: false,
|
|
multiline: false,
|
|
icon: "phone",
|
|
get name() {
|
|
return $l("Phone Number");
|
|
},
|
|
},
|
|
[FieldType.Pin]: {
|
|
type: FieldType.Pin,
|
|
pattern: /.*/,
|
|
matchPattern: /\d+/,
|
|
mask: true,
|
|
multiline: false,
|
|
icon: "lock",
|
|
get name() {
|
|
return $l("PIN");
|
|
},
|
|
format(value, masked) {
|
|
return masked ? value.replace(/./g, "\u2022") : value;
|
|
},
|
|
},
|
|
[FieldType.Text]: {
|
|
type: FieldType.Text,
|
|
pattern: /.*/,
|
|
matchPattern: /.*/,
|
|
mask: false,
|
|
multiline: true,
|
|
icon: "text",
|
|
get name() {
|
|
return $l("Plain Text");
|
|
},
|
|
},
|
|
[FieldType.Note]: {
|
|
type: FieldType.Note,
|
|
pattern: /.*/,
|
|
matchPattern: /(.*)(\n)?(.*)/,
|
|
mask: false,
|
|
multiline: true,
|
|
icon: "note",
|
|
get name() {
|
|
return $l("Richtext / Markdown");
|
|
},
|
|
format(value: string) {
|
|
return value.split("\n")[0] || "";
|
|
},
|
|
},
|
|
[FieldType.Totp]: {
|
|
type: FieldType.Totp,
|
|
pattern: /^([A-Z2-7=]{8})+$/i,
|
|
matchPattern: /^([A-Z2-7=]{8})+$/i,
|
|
mask: false,
|
|
multiline: false,
|
|
icon: "totp",
|
|
get name() {
|
|
return $l("One-Time Password");
|
|
},
|
|
async transform(value: string) {
|
|
return await totp(base32ToBytes(value));
|
|
},
|
|
},
|
|
};
|
|
|
|
export class Field extends Serializable {
|
|
constructor(vals: Partial<Field> = {}) {
|
|
super();
|
|
Object.assign(this, vals);
|
|
}
|
|
|
|
/**
|
|
* field type, determining meta data via the corresponding field definition
|
|
* in [[FIELD_DEFS]]
|
|
*/
|
|
type: FieldType = FieldType.Text;
|
|
/** field name */
|
|
name: string = "";
|
|
/** field content */
|
|
value: string = "";
|
|
|
|
get def(): FieldDef {
|
|
return FIELD_DEFS[this.type] || FIELD_DEFS[FieldType.Text];
|
|
}
|
|
|
|
get icon() {
|
|
return this.def.icon;
|
|
}
|
|
|
|
async transform() {
|
|
return this.def.transform ? await this.def.transform(this.value) : this.value;
|
|
}
|
|
|
|
format(masked: boolean) {
|
|
return this.def.format ? this.def.format(this.value, masked) : this.value;
|
|
}
|
|
|
|
protected _fromRaw(raw: any) {
|
|
if (!raw.type) {
|
|
raw.type = guessFieldType(raw);
|
|
}
|
|
return super._fromRaw(raw);
|
|
}
|
|
}
|
|
|
|
/** Normalizes a tag value by removing invalid characters */
|
|
export function normalizeTag(tag: string): Tag {
|
|
return tag.replace(",", "");
|
|
}
|
|
|
|
export enum AuditType {
|
|
WeakPassword = "weak_password",
|
|
ReusedPassword = "reused_password",
|
|
CompromisedPassword = "compromised_password",
|
|
}
|
|
|
|
export interface AuditResult {
|
|
type: AuditType;
|
|
fieldIndex: number;
|
|
}
|
|
|
|
/** Represents an entry within a vault */
|
|
export class VaultItem extends Serializable {
|
|
constructor(vals: Partial<VaultItem> = {}) {
|
|
super();
|
|
Object.assign(this, vals);
|
|
}
|
|
|
|
/** unique identfier */
|
|
id: VaultItemID = "";
|
|
|
|
/** item name */
|
|
name: string = "";
|
|
|
|
/** icon to be displayed for this item */
|
|
icon?: string = undefined;
|
|
|
|
/** item fields */
|
|
@AsSerializable(Field)
|
|
fields: Field[] = [];
|
|
|
|
/** array of tags assigned with this item */
|
|
tags: Tag[] = [];
|
|
|
|
/** Date and time of last update */
|
|
@AsDate()
|
|
updated: Date = new Date();
|
|
|
|
/** [[Account]] the item was last updated by */
|
|
updatedBy: AccountID = "";
|
|
|
|
/**
|
|
* @DEPRECATED
|
|
* Accounts that have favorited this item
|
|
*/
|
|
favorited: AccountID[] = [];
|
|
|
|
/** attachments associated with this item */
|
|
@AsSerializable(AttachmentInfo)
|
|
attachments: AttachmentInfo[] = [];
|
|
|
|
auditResults: AuditResult[] = [];
|
|
|
|
@AsDate()
|
|
lastAudited?: Date;
|
|
}
|
|
|
|
/** Creates a new vault item */
|
|
export async function createVaultItem({
|
|
name = "Unnamed",
|
|
fields = [],
|
|
tags = [],
|
|
icon,
|
|
}: Partial<VaultItem>): Promise<VaultItem> {
|
|
return new VaultItem({
|
|
name,
|
|
fields,
|
|
tags,
|
|
icon,
|
|
id: await uuid(),
|
|
});
|
|
}
|
|
|
|
/** Guesses the most appropriate field type based on field name and value */
|
|
export function guessFieldType({
|
|
name,
|
|
value = "",
|
|
masked = false,
|
|
}: {
|
|
name: string;
|
|
value?: string;
|
|
masked?: boolean;
|
|
}): FieldType {
|
|
if (masked) {
|
|
return FieldType.Password;
|
|
}
|
|
|
|
const matchedTypeByName = Object.keys(FIELD_DEFS).filter((fieldType) =>
|
|
new RegExp(fieldType, "i").test(name)
|
|
)[0] as FieldType;
|
|
|
|
if (matchedTypeByName) {
|
|
return matchedTypeByName;
|
|
}
|
|
|
|
// We skip some because they can match anything, and are only really valuable when matched by name
|
|
const fieldTypesToSkipByValue = [FieldType.Username, FieldType.Password];
|
|
|
|
const matchedTypeByValue = Object.keys(FIELD_DEFS)
|
|
// @ts-ignore this is a string, deal with it, TypeScript (can't `as` as well)
|
|
.filter((fieldType) => !fieldTypesToSkipByValue.includes(fieldType))
|
|
.filter((fieldType) => FIELD_DEFS[fieldType].matchPattern.test(value))[0] as FieldType;
|
|
|
|
if (value !== "" && matchedTypeByValue) {
|
|
return matchedTypeByValue;
|
|
}
|
|
|
|
return FieldType.Text;
|
|
}
|
|
|
|
export interface ItemTemplate {
|
|
name?: string;
|
|
fields: { name: string; value?: string; type: FieldType }[];
|
|
icon: string;
|
|
iconSrc?: string;
|
|
toString(): string;
|
|
subTitle?: string;
|
|
attachment?: boolean;
|
|
}
|
|
|
|
export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|
{
|
|
toString: () => $l("Website / App"),
|
|
icon: "web",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("Username");
|
|
},
|
|
type: FieldType.Username,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Password");
|
|
},
|
|
type: FieldType.Password,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("URL");
|
|
},
|
|
type: FieldType.Url,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("Computer"),
|
|
icon: "desktop",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("Username");
|
|
},
|
|
type: FieldType.Username,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Password");
|
|
},
|
|
type: FieldType.Password,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("Credit Card"),
|
|
icon: "credit",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("Card Number");
|
|
},
|
|
type: FieldType.Credit,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Card Owner");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Valid Until");
|
|
},
|
|
type: FieldType.Month,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("CVC");
|
|
},
|
|
type: FieldType.Pin,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("PIN");
|
|
},
|
|
type: FieldType.Pin,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("Bank Account"),
|
|
icon: "bank",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("Account Owner");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("IBAN");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("BIC");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Card PIN");
|
|
},
|
|
type: FieldType.Pin,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("WIFI Password"),
|
|
icon: "wifi",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("Name");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Password");
|
|
},
|
|
type: FieldType.Password,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("Passport"),
|
|
icon: "passport",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("Full Name");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Passport Number");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Country");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Birthdate");
|
|
},
|
|
type: FieldType.Date,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Birthplace");
|
|
},
|
|
type: FieldType.Text,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Issued On");
|
|
},
|
|
type: FieldType.Date,
|
|
},
|
|
{
|
|
get name() {
|
|
return $l("Expires");
|
|
},
|
|
type: FieldType.Date,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("Note"),
|
|
icon: "note",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("Note");
|
|
},
|
|
type: FieldType.Note,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("Authenticator"),
|
|
icon: "totp",
|
|
fields: [
|
|
{
|
|
get name() {
|
|
return $l("One-Time Password");
|
|
},
|
|
type: FieldType.Totp,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
toString: () => $l("Document"),
|
|
icon: "attachment",
|
|
fields: [],
|
|
attachment: true,
|
|
},
|
|
{
|
|
toString: () => $l("Custom"),
|
|
icon: "custom",
|
|
fields: [],
|
|
},
|
|
];
|