buttercup / buttercup-core

:tophat: The mighty NodeJS password vault
http://buttercup.pw/
MIT License
469 stars 57 forks source link

Sample vault reading code in README.md does not work as suggested in NodeJS. #326

Open thaining opened 1 year ago

thaining commented 1 year ago

Code in the README.md to read Vault content does not work as suggested in NodeJS.

The README suggests that following code sample should work:

const { Credentials, FileDatasource, Vault, init } = require("buttercup");

init();

const datasourceCredentials = Credentials.fromDatasource({
    path: "./user.bcup"
}, "masterPassword!");
const fileDatasource = new FileDatasource(datasourceCredentials);

fileDatasource
    .load(datasourceCredentials)
    .then(Vault.createFromHistory)
    .then(vault => {
        // ...
    });

This hits two problems. The first problem is with the integrity of the datasourceCredentials. Running the above snippet with node and a valid buttercup file produces the following error during load():

/home/thaining/tmp/builder/creator/node_modules/buttercup/dist/datasources/TextDatasource.js:142
            return Promise.reject(new Error("Provided credentials don't allow vault decryption"));
                                  ^

Error: Provided credentials don't allow vault decryption
    at FileDatasource.load (/home/thaining/tmp/builder/creator/node_modules/buttercup/dist/datasources/TextDatasource.js:142:35)
    at /home/thaining/tmp/builder/creator/node_modules/buttercup/dist/datasources/FileDatasource.js:103:30

This appears to be cause of imprecise use of copy-by-sharing in the TextDatasource constructor:

    constructor(credentials: Credentials) {
        super();
        this._credentials = credentials;
        this._credentials.restrictPurposes([Credentials.PURPOSE_SECURE_EXPORT]);
        this._content = "";

restrictPurposes() modifies an object called by reference, not a private copy. The object only has a 'secure-export' purpose, which causes fileDatasource.load() to fail.

An ugly workaround is to create another copy of the object.

import { Credentials, FileDatasource, Vault, init } from "buttercup";

init();

const datasourceCredentials = Credentials.fromDatasource({
    path: "./user.bcup"
}, "masterPassword!");
const loadCredentials = Credentials.fromDatasource({
    path: "./user.bcup"
}, "masterPassword!");
const fileDatasource = new FileDatasource(datasourceCredentials);

fileDatasource
    .load(loadCredentials)
    .then(Vault.createFromHistory)
    .then(vault => {
        // ...
    });

This reveals a second error:

/home/thaining/tmp/builder/creator/node_modules/buttercup/dist/io/VaultFormatA.js:375
            throw new Error(`Invalid command: ${command}`);
                  ^

Error: Invalid command: [object Object]
    at VaultFormatA._executeCommand (/home/thaining/tmp/builder/creator/node_modules/buttercup/dist/io/VaultFormatA.js:375:19)
    at /home/thaining/tmp/builder/creator/node_modules/buttercup/dist/io/VaultFormatA.js:208:42
    at Array.forEach (<anonymous>)
    at VaultFormatA.execute (/home/thaining/tmp/builder/creator/node_modules/buttercup/dist/io/VaultFormatA.js:208:18)
    at Function.createFromHistory (/home/thaining/tmp/builder/creator/node_modules/buttercup/dist/core/Vault.js:30:22)
    at /home/thaining/tmp/builder/creator/reader.js:18:22

This occurs because of how TextDataSource.load() eventually returns the Vault() history:

        const Format = detectFormat(this._content);
        return Format.parseEncrypted(this._content, credentials).then((history: History) => ({
            Format,
            history
        }));

It's returning an anonymous object, not a DatasourceLoadedData. The compiled javascript sees this as single returned object with history and Format members. The undefined Format member passed back by load() is tolerated by createFromHistory(), but the object passed in is not a valid History. Execution fails as soon as that non-History object is used.

    static createFromHistory(history: History, format: any = getDefaultFormat()): Vault {
        const vault = new Vault(format);
        vault.format.erase();
        vault.format.execute(history);

vault.format in my case resolves to VaultFormatA, and _executeCommand() does not know what to do with it.

    execute(commandOrCommands: string | Array<string>) {
        if (this.readOnly) {
            throw new Error("Format is in read-only mode");
        }
        const commands = Array.isArray(commandOrCommands) ? commandOrCommands : [commandOrCommands];
        commands.forEach(command => this._executeCommand(command));

I'm not much of a typescript writer, but I tried compiling a typescript version of the snippet. The compiler recognized the same problem immediately:

reader.ts:12:11 - error TS2345: Argument of type '(history: History, format?: any) => Vault' is not assignable to parameter of type '(value: DatasourceLoadedData) => Vault | PromiseLike<Vault>'.
  Types of parameters 'history' and 'value' are incompatible.
    Type 'DatasourceLoadedData' is missing the following properties from type 'History': length, pop, push, concat, and 16 more.

12     .then(Vault.createFromHistory)

Doing this resolves the issue for NodeJS:

fileDatasource
    .load(fooDatasourceCredentials)
    .then(datasourceLoadedData => {
        return Vault.createFromHistory(datasourceLoadedData.history,
                datasourceLoadedData.Format);
    })
    .then(vault => {
fkasler commented 1 year ago

Check out this dump utility. I'm sure you can tweak the logic for your needs. Buttercup 'a' format is a little messy, but 'b' format is just nested JSON: https://github.com/fkasler/butterbrute/blob/main/dump_vault.mjs