buttercup-pw/buttercup-core

View on GitHub
source/datasources/TextDatasource.ts

Summary

Maintainability
A
0 mins
Test Coverage
import EventEmitter from "eventemitter3";
import hash from "hash.js";
import { Credentials } from "../credentials/Credentials.js";
import { credentialsAllowsPurpose, getCredentials } from "../credentials/memory/credentials.js";
import { detectFormat, getFormatForID } from "../io/formatRouter.js";
import { fireInstantiationHandlers, registerDatasource } from "./register.js";
import {
    AttachmentDetails,
    BufferLike,
    DatasourceConfiguration,
    DatasourceLoadedData,
    EncryptedContent,
    History,
    VaultID,
    VaultInsights,
    VaultSourceID
} from "../types.js";

/**
 * Datasource for text input and output
 * @memberof module:Buttercup
 */
export class TextDatasource extends EventEmitter {
    _content: EncryptedContent;
    _credentials: Credentials;

    public sourceID: VaultSourceID = null;
    public type: string;

    /**
     * Constructor for the text datasource
     * @param credentials The credentials and configuration for
     *  the datasource
     */
    constructor(credentials: Credentials) {
        super();
        this._credentials = credentials;
        this._credentials.restrictPurposes([Credentials.PURPOSE_SECURE_EXPORT]);
        this._content = "";
        try {
            const { data: credentialData } = getCredentials(credentials.id);
            const { datasource: datasourceConfig = {} } = credentialData || {};
            const { content = "" } = datasourceConfig as DatasourceConfiguration;
            this._content = content;
        } catch (err) {}
        this.type = "text";
        fireInstantiationHandlers("text", this);
    }

    /**
     * Datasource credentials
     * @readonly
     */
    get credentials(): Credentials {
        return this._credentials;
    }

    /**
     * Whether the datasource currently has content
     * Used to check if the datasource has encrypted content that can be
     * loaded. May be used when attempting to open a vault in offline mode.
     * @memberof TextDatasource
     */
    get hasContent(): boolean {
        return this._content && this._content.length > 0;
    }

    /**
     * Change the password of the vault
     * @param newCredentials The new credentials to take the new password from
     * @param preflight Whether or not the password change attempt is a preflight
     *  check or not
     * @returns True/False if in preflight, as to whether or not the password change
     *  can be performed at this time, or undefined when not in preflight mode.
     */
    async changePassword(
        newCredentials: Credentials,
        preflight: boolean
    ): Promise<boolean | undefined> {
        throw new Error("Changing password not supported");
    }

    /**
     * Get attachment buffer
     * - Downloads the attachment contents into a buffer
     * @param vaultID The ID of the vault
     * @param attachmentID The ID of the attachment
     * @memberof TextDatasource
     */
    getAttachment(vaultID: VaultID, attachmentID: string): Promise<BufferLike> {
        return Promise.reject(new Error("Attachments not supported"));
    }

    /**
     * Get the available storage space, in bytes
     * @returns Bytes of free space, or null if not
     *  available
     * @memberof TextDatasource
     */
    getAvailableStorage(): Promise<number | null> {
        return Promise.resolve(null);
    }

    /**
     * Get the datasource configuration
     * @memberof TextDatasource
     */
    getConfiguration(): DatasourceConfiguration {
        return {
            type: "text",
            content: this._content
        };
    }

    /**
     * Get the total storage space, in bytes
     * @returns Bytes of free space, or null if not
     *  available
     * @memberof TextDatasource
     */
    getTotalStorage(): Promise<number | null> {
        return Promise.resolve(null);
    }

    /**
     * Get the ID of the datasource
     * ID to uniquely identify the datasource and its parameters
     * @returns A hasn of the datasource (unique ID)
     * @memberof TextDatasource
     */
    getID(): string {
        const content = this.type === "text" ? this._content : this.toString();
        if (!content) {
            throw new Error("Failed getting ID: Datasource requires content for ID generation");
        }
        return hash.sha256().update(content).digest("hex");
    }

    /**
     * Load from the stored content using a password to decrypt
     * @param credentials The password or Credentials instance to decrypt with
     * @returns A promise that resolves with decrypted history
     * @throws {Error} Rejects if content is empty
     * @memberof TextDatasource
     */
    load(credentials: Credentials): Promise<DatasourceLoadedData> {
        if (!this._content) {
            return Promise.reject(new Error("Failed to load vault: Content is empty"));
        }
        if (credentialsAllowsPurpose(credentials.id, Credentials.PURPOSE_DECRYPT_VAULT) !== true) {
            return Promise.reject(new Error("Provided credentials don't allow vault decryption"));
        }
        const Format = detectFormat(this._content);
        return Format.parseEncrypted(this._content, credentials).then((history: History) => ({
            Format,
            history
        }));
    }

    /**
     * Put attachment data
     * @param vaultID The ID of the vault
     * @param attachmentID The ID of the attachment
     * @param buffer The attachment data
     * @param details Attachment details object
     * @memberof TextDatasource
     */
    putAttachment(
        vaultID: VaultID,
        attachmentID: string,
        buffer: BufferLike,
        details: AttachmentDetails
    ): Promise<void> {
        return Promise.reject(new Error("Attachments not supported"));
    }

    /**
     * Remove an attachment
     * @param vaultID The ID of the vault
     * @param attachmentID The ID of the attachment
     * @memberof TextDatasource
     */
    removeAttachment(vaultID: VaultID, attachmentID: string): Promise<void> {
        return Promise.reject(new Error("Attachments not supported"));
    }

    /**
     * Save archive contents with a password
     * @param history Archive history to save
     * @param credentials The Credentials instance to encrypt with
     * @returns A promise resolving with the encrypted content
     * @memberof TextDatasource
     */
    async save(history: History, credentials: Credentials): Promise<EncryptedContent> {
        if (credentialsAllowsPurpose(credentials.id, Credentials.PURPOSE_ENCRYPT_VAULT) !== true) {
            throw new Error("Unable to save: Provided credentials don't allow vault encryption");
        }
        if (!history.format) {
            throw new Error("Unable to save: Provided history does not contain a format");
        }
        const content = await getFormatForID(history.format).encodeRaw(history, credentials);
        this.emit("encryptedContent", { content });
        return content;
    }

    /**
     * Set the text content
     * @param content The encrypted text content
     * @returns Self
     * @memberof TextDatasource
     */
    setContent(content: EncryptedContent): this {
        this._content = content || "";
        return this;
    }

    /**
     * Whether or not the datasource supports attachments
     * @memberof TextDatasource
     */
    supportsAttachments(): boolean {
        return false;
    }

    /**
     * Whether or not the datasource supports the changing of the master password
     * @returns True if the datasource supports password changing
     * @memberof TextDatasource
     */
    supportsPasswordChange(): boolean {
        return false;
    }

    /**
     * Whether or not the datasource supports bypassing remote fetch operations
     *  (offline support)
     * @returns True if content can be set to bypass fetch operations,
     *  false otherwise
     * @memberof TextDatasource
     */
    supportsRemoteBypass(): boolean {
        return false;
    }

    /**
     * Record vault insights, if supported, to some destination
     * @param insights Vault insights data
     * @memberof TextDatasource
     */
    updateInsights(insights: VaultInsights): Promise<void> {
        return Promise.resolve();
    }
}

registerDatasource("text", TextDatasource);